Connect a Claude Code session#
Claude Code sessions can search, read and write scrap through its MCP server (see
MCP server). Nothing is installed on the session’s machine: it talks to /mcp.
How the session reaches /mcp depends on where scrap runs:
flowchart LR
CC["Claude Code"] -->|"http://localhost:8000"| L["scrap, same machine"]
CC -->|"HTTPS, tailnet"| T["Tailscale Serve / operator"]
CC -->|"HTTP, home network"| N["scrap on LAN"]
CC -->|"bearer + service token"| CF["Cloudflare Access"]
L --> M["/mcp"]
T --> M
N --> M
CF --> M
classDef claude fill:#fff3e0,stroke:#e08a00
classDef store fill:#e8f1ff,stroke:#4a7bd0
class CC claude
class L,T,N,CF,M store
Tailnet: HTTPS (Install on a server with Tailscale). Home network: plain HTTP (Try scrap on your LAN). Kubernetes: Cloudflare Access with a bearer token (see “If
/mcpneeds a token” below, Install scrap on Kubernetes and ADR 0023).Claude Code in claude-sandbox: follow Connect Claude Code in claude-sandbox instead; it reaches scrap on a port of the host.
On your own machine, run scrap as in “Try it” in the README and add
http://localhost:8000/mcpin step 2; step 1 is not needed on loopback.
Before you start you need:
scrap running and reachable from the machine running Claude Code: on the same machine or LAN (Try scrap on your LAN), published on your tailnet (see Install on a server with Tailscale, Deploy on a single server, “Publish on your tailnet”, or Tailscale Kubernetes operator (recommended)), or on Kubernetes behind Cloudflare Access (Install scrap on Kubernetes);
the Claude Code CLI on that machine;
optionally, ingest on (
SCRAP_INGEST=on) if you want Claude to describe photos (see step 2).
1. Tell scrap its address#
/mcp answers only requests addressed to loopback or to SCRAP_PUBLIC_URL. Put the address
that Tailscale Serve gives scrap (the --https port you chose when you published it; 8446 is
only an example) in ~/.config/scrap/ingest.env, scrap’s own settings file (keep it mode 600;
install.sh creates it so), for example:
SCRAP_PUBLIC_URL=https://myhost.my-tailnet.ts.net:8446
then systemctl --user restart scrap. Run both as the dedicated scrap user, in a real login
session (ssh scrap@<host>, or sudo machinectl shell scrap@; see Deploy on a single server).
The address must be a full URL with the scheme, and must match what clients use, port included:
a request whose Host differs gets 421 Invalid Host header. You can give several addresses,
comma separated; the first makes the page links the tools return. Without it, those links are
bare /p/<slug> paths that cannot be clicked.
On Kubernetes the chart has no setting of its own for it: pass it with extraEnv, using the
Tailscale operator’s address (no port, since it serves on 443):
extraEnv:
- name: SCRAP_PUBLIC_URL
value: https://scrap.<tailnet>.ts.net
2. Add the server to Claude Code#
On a machine on the tailnet:
claude mcp add --transport http --scope user scrap https://myhost.my-tailnet.ts.net:8446/mcp
The default scope is local (this project, only you); --scope user makes it available in all
your projects. Avoid --scope project: it writes the tailnet hostname into a .mcp.json that
you would commit. claude mcp list should show scrap as connected. claude mcp remove scrap
takes it out again.
The server has eighteen tools (four fewer with SCRAP_MCP_CONTENT_EDIT=0, which drops edit_page, edit_drawing, trash_pages and undelete, and one fewer with SCRAP_MCP_UPLOAD=0, which drops add_svg), which appear as mcp__scrap__search (it also lists the recent
pages and the todos), mcp__scrap__read, mcp__scrap__current_page, mcp__scrap__drawing,
mcp__scrap__tags, mcp__scrap__topics,
mcp__scrap__write, mcp__scrap__writeup, mcp__scrap__edit_meta, mcp__scrap__rename_tag,
mcp__scrap__revisions, mcp__scrap__revert, mcp__scrap__edit_page,
mcp__scrap__edit_drawing, mcp__scrap__add_svg, mcp__scrap__trash, mcp__scrap__trash_pages
and mcp__scrap__undelete; the Tools table in MCP server says what each takes and
returns.
Try it: ask “get my recent photos”. Search results say which pages hold images (image_count,
and the first image’s kind, description and thumbnail URL), and has:image or image:photo
lists them newest first; see MCP server. The kind and description are there only
once ingest has read the photo (SCRAP_INGEST=on). The image URLs are the browser’s and can be
fetched only from the tailnet; results also give download URLs under /mcp that take the same
headers as /mcp (see “Downloading files” below).
Check it from a shell without Claude:
curl -si https://myhost.my-tailnet.ts.net:8446/mcp \
-H 'Accept: application/json, text/event-stream' -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
It should print an HTTP 200 status and JSON with fifteen tools in result.tools.
Off the tailnet, or for a quick test, use the SSH tunnel fallback (Fallback: an SSH tunnel) and
add http://127.0.0.1:8790/mcp instead; loopback needs no SCRAP_PUBLIC_URL. A session that
cannot reach the tailnet (a hosted or cloud Claude session, claude.ai) cannot use scrap this way,
unless scrap is published through Cloudflare Access with a service token and a bearer token (below).
If /mcp needs a token#
If scrap has SCRAP_MCP_TOKEN or SCRAP_MCP_TOKEN_FILE set (see MCP server), or sits
behind Cloudflare Access, the session must send secrets as headers: the bearer token and, behind
Access, the service token (CF_ACCESS_CLIENT_ID and CF_ACCESS_CLIENT_SECRET). The recommended
route is a headersHelper script that reads an env file, because it works in a devcontainer or
a jail where nothing exports those variables, and the secrets stay out of every shell’s
environment.
1. The env file. Put the values in one file that is never committed, for example
~/.config/terminal-config/.env, and make it private:
CF_ACCESS_CLIENT_ID=<client id>
CF_ACCESS_CLIENT_SECRET=<client secret>
SCRAP_MCP_TOKEN=<the token>
chmod 600 ~/.config/terminal-config/.env
Leave out the two CF_ lines if scrap is not behind Cloudflare Access. This file is the one
place to rotate a token: the next connection reads the new value, with no claude mcp change.
2. The helper. Claude Code runs this each time it connects and reads the headers it prints as
JSON. Save it as ~/.claude/scrap-headers.sh and make it executable (chmod 700):
#!/bin/sh
# Headers for scrap's /mcp: Cloudflare Access service token + bearer token
set -a; . ~/.config/terminal-config/.env; set +a
printf '{"CF-Access-Client-Id":"%s","CF-Access-Client-Secret":"%s","Authorization":"Bearer %s"}\n' \
"$CF_ACCESS_CLIENT_ID" "$CF_ACCESS_CLIENT_SECRET" "$SCRAP_MCP_TOKEN"
Without Cloudflare Access, print only {"Authorization":"Bearer %s"}.
The printf does no JSON escaping, so a secret containing a double quote or a backslash would
break the output (real tokens are alphanumeric or hex, so this does not normally arise). The
helper sources the env file as shell, so a value with spaces or a $ must be quoted in the file.
3. Register it. In ~/.claude.json (user scope), under mcpServers, with the full absolute
path, since ~ may not expand there:
"scrap": {
"type": "http",
"url": "https://scrap.example.com/mcp",
"headersHelper": "/home/<you>/.claude/scrap-headers.sh"
}
4. Check it. claude mcp list should show scrap as connected. To check the helper on its
own, confirm it prints valid JSON without showing the values:
~/.claude/scrap-headers.sh | python3 -c 'import json,sys; print(sorted(json.load(sys.stdin)))'
It should print the header names only. Do not paste the helper’s raw output into an issue or a log, and keep the env file out of git.
In claude-sandbox or a devcontainer. claude-sandbox mounts the host’s
~/.config/terminal-config at /user-terminal-config in the container, and from there read-only
into the jail, and keeps ~/.claude and ~/.claude.json in the container too (see
Use the container image
and Deliberately exposed).
So put the env file at ~/.config/terminal-config/.env on the host, and have the helper source
/user-terminal-config/.env instead. Keep the helper at ~/.claude/scrap-headers.sh and
register scrap with user scope (--scope user, or in ~/.claude.json): both then reach every
project container and survive claude-sandbox --recreate, with nothing to set up per project.
If scrap is on a private or loopback address instead, see Connect Claude Code in claude-sandbox for the
local-port and allow-ip settings.
A devcontainer that is not claude-sandbox needs the same bind mount
(${localEnv:HOME}/.config/terminal-config to /user-terminal-config), as in scrap’s own
.devcontainer/devcontainer.json.
The simple alternative. In a shell where the variables are exported, Claude Code expands
${VAR} in header values. With claude mcp add:
claude mcp add --transport http scrap https://myhost.my-tailnet.ts.net:8446/mcp \
--header 'Authorization: Bearer ${SCRAP_MCP_TOKEN}'
or in .mcp.json:
{
"mcpServers": {
"scrap": {
"type": "http",
"url": "https://scrap.example.com/mcp",
"headers": {"Authorization": "Bearer ${SCRAP_MCP_TOKEN}"}
}
}
}
Behind Cloudflare Access, add CF-Access-Client-Id: ${CF_ACCESS_CLIENT_ID} and
CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET} too. If a variable is not set in the process
that starts claude, the header goes out empty and scrap answers 401 or 403; use the helper
above when that can happen.
Downloading files#
read gives each image and file a download_url, an image with a drawing a
drawing_download_url (the whole drawing as an .excalidraw file), and the page a
download_url (the page as .md, or with its files as .zip); search rows have
image_download_url and drawing a download_url (the page and drawing ones are null for a
page in the trash). They are under /mcp
(/mcp/blobs/<sha>, /mcp/pages/<id>/download, …), so they need the same headers as the
MCP connection: the bearer token if scrap has one, and behind Cloudflare Access the service
token, which the /mcp application already covers. No other Access application or setting is
needed. Fetch one with the values from the env file above:
set -a; . ~/.config/terminal-config/.env; set +a
curl -fsS -OJ "<download_url>" -H "Authorization: Bearer $SCRAP_MCP_TOKEN" \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"
Without a token or Access, a plain curl -OJ <download_url> works. They are read only; the
browser’s url links are unchanged. See Downloads in the reference.
Adding images and files#
Two ways, both on unless the server sets SCRAP_MCP_UPLOAD=0, and both Claude’s changes (a new
text revision by claude: is:claude-edited finds the page, and /sc-undo or revert takes
the embed out again):
A diagram Claude draws:
add_svg. Ask for one (“add a labelled diagram of the triangle under question 1”) and Claude writes an SVG and callsadd_svgwith the page, the markup, alt text and, to place it,after: text from the page (the end of the question) that occurs once. The diagram shows in the PWA like a photo, and its labels are searchable. scrap stores it sanitised (no scripts, no links out); see Adding images and files.A file that exists already: the upload. A PNG from matplotlib, a screenshot or a PDF goes up as a file, with the same headers as the downloads, to
/mcp/pages/<slug>/captures, the PWA’s Add photo route under/mcp.idis a ULID, the retry key: send the same one to retry.
set -a; . ~/.config/terminal-config/.env; set +a
id=$(python3 -c 'import os,time;n=int(time.time()*1000)<<80|int.from_bytes(os.urandom(10),"big");print("".join("0123456789ABCDEFGHJKMNPQRSTVWXYZ"[n>>s&31] for s in range(125,-1,-5)))')
curl -fsS "https://scrap.example.com/mcp/pages/<slug>/captures" \
-H "Authorization: Bearer $SCRAP_MCP_TOKEN" \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" -H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
-F id="$id" -F file=@chart.png -F alt="Speed against time" -F after="Q3 Plot the graph."
The reply (201, or 200 for a retry) is the page, with added: each file’s download_url.
An image’s embed goes right after the line after ends on, inside its list item or quote (or at the end of the text), with the alt
given; a PDF or text file is listed below the text and read for search.
If it does not connect#
401: scrap wants a bearer token and the client sent none, or a wrong one. Check the header above; acurlcheck needs-H "Authorization: Bearer $TOKEN"too.421 Invalid Host header: theHostthe client used is not loopback or inSCRAP_PUBLIC_URL(unset, wrong, or a different port). Fix step 1 and restart scrap.403: the request carried anOriginthat is not one of those addresses.claude mcp listshowsscrapas failed: the machine is off the tailnet, nothing is published at that address, or the tailnet ACL blocks the port. Check the address itself with./deploy/smoke.sh <url>; see Troubleshooting and the Tailnet ACL section of Deploy on a single server.It worked and stopped after the address changed (a new
SCRAP_PUBLIC_URL, another Tailscale Serve port, or a move to the Kubernetes ingress): scrap readsSCRAP_PUBLIC_URLonly at startup, so restart it (systemctl --user restart scrap; on Kubernetes,helm upgraderolls the pod) for the new address to be accepted. Claude Code keeps the URL you registered and does not rewrite it, so register the new one:claude mcp remove scrap, then theclaude mcp addof step 2 with the new<url>/mcp(and the same--scope). A restart of scrap alone needs nothing in the session:/mcpkeeps no session state, so the next call just works.413: a request over 2 MiB.A tool error such as “only a page written over MCP can be appended to”, or a title over 200 characters, a body over 200 kB or more than 20 tags: the call is refused and nothing is saved.
3. Install the sc commands and the skills#
commands/ in this repository holds Claude Code slash commands that give you short scrap
commands. Each is one Markdown file with a one-line description, and each is its own command, so
typing /sc in Claude Code autocompletes the whole set with that description. They use the server added in step 2 (the mcp__scrap__* tools) and
do nothing useful without it; if the tools are missing they say so and print the claude mcp add
line.
Command |
What it does |
|---|---|
|
Posts Claude’s previous response to scrap verbatim, as one page. “post this to scrap” works too. |
|
Summarises the current topic (the latest activity) into one page and posts it (below). With |
|
Searches scrap and shows the top results as short lines. |
|
What to do now, overdue first. |
|
Bulk kind and tag changes with |
|
Lists recent |
|
Writes one new page from your instructions, picking out only what you ask for from the conversation (“the URLs we discussed and why”). Uses only the conversation, lists anything uncertain under Unsure, and reuses existing tags. Always creates a page. |
|
Answers from scrap alone: searches, reads the pages, and cites them as links; says “not in scrap” rather than using general knowledge. Read-only. |
|
Changes the text of an existing page with |
|
Ticks off a todo’s task boxes with |
|
What changed lately (default |
|
An index: searches like |
Install them for all your projects with one command:
curl -fsSL https://raw.githubusercontent.com/gilesknap/scrap/main/commands/install.sh | sh
It copies the files into ~/.claude/commands (set CLAUDE_COMMANDS_DIR to put them elsewhere, such
as .claude/commands for one project). From a clone of this repository you can instead copy
commands/*.md yourself, or symlink them to keep up to date with the repository:
ln -sfn "$PWD"/commands/*.md ~/.claude/commands/. Start a new Claude Code session afterwards,
and type /sc to check that the commands are listed. Copy the files unchanged.
The same command installs the scrap-links skill (skills/scrap-links in this repository).
It makes every scrap item an answer surfaces a clickable link, in any question you ask about
scrap, not only in the sc commands: a todo, note or search result is shown as
[Title](url), never as a bare slug or plain title. It goes into
~/.claude/skills/scrap-links (set CLAUDE_SKILLS_DIR to put it elsewhere, such as
.claude/skills for one project). From a clone you can copy the folder yourself, or symlink it:
ln -sfn "$PWD"/skills/scrap-links ~/.claude/skills/. To check it is loaded, start a new Claude
Code session: scrap-links is listed when you ask “what skills do you have”,
and asking “what todos are for today” lists every todo as a link.
These commands replace the earlier single /s <verb> skill (/s post, /s tidy, and so on), the
s- and sc- skill sets that briefly preceded them, and the older scrap-post skill. Delete
~/.claude/skills/s, ~/.claude/skills/s-*, ~/.claude/skills/sc and ~/.claude/skills/sc-*
(a skill and a command of the same name clash) and ~/.claude/skills/scrap-post after installing
these. The scrap-to-gist skill, which earlier releases installed, is removed
too (ADR 43:
public pages, still being built, are planned to take its place); the installer leaves an
installed copy alone, so delete ~/.claude/skills/scrap-to-gist when you no longer want it.
Only the scrap tools are used. <SCRAP_URL> in the
commands is a placeholder (the hint they give when the tools are missing, and the shape of the
page URL they report); the real URL comes from the write tool, built from SCRAP_PUBLIC_URL.
/sc-summarise makes Claude summarise the useful findings of the current topic into one page (/sc-summarise all for the whole
conversation), reusing existing tags and linking related pages, and show you the title and tags before it
writes; say “just post” to skip the question.
The summary has a fixed layout: a one-line summary, then Findings, Decisions, Details and Open
questions. It is a note unless it is a task (todo) or a longer reference (doc), carries
the repository and date as its origin, and is sent with a random key so that a retry does not
post twice. The command tells Claude to leave out secrets, credentials and private data. It
appends to an earlier page only if that page was written by Claude (written_by_claude in
read); otherwise it links to your page.
Pages written this way carry a Written by Claude badge and the origin the session gave;
is:claude lists them. Claude never deletes a page for good: it can move pages you name to the trash
(trash_pages, undone with undelete), and the nightly gc purges them after SCRAP_TRASH_DAYS days. It can change any page’s kind and tags
(“make these 15 notes whiteboards”, with edit_meta); each such change is a revision you can
ask it to list (revisions) and undo (revert). It can also edit the text of a page you name
(“fix the typo in my trip notes”, with edit_page): it replaces one passage it quotes exactly,
shows you a diff first if you ask, and refuses if the page changed since it read it. It can save a refined drawing too (edit_drawing, a new version in the drawing’s history). Each text
edit is a revision too, which revert (with to_rev) undoes, and is:claude-edited finds the pages whose
text it changed. A bulk change
given as a search is refused if it finds more than 50 pages unless limit is raised (up to
200; naming the pages takes up to 200 as well), and Claude is told to try dry_run first. See “Kinds and tags” in MCP server.
Work on a phone page from Claude Code#
The Claude Android app cannot use scrap’s MCP server, so a page you are looking at on your phone reaches Claude Code through the clipboard:
On the phone, copy the page’s URL (or share it to a note or message you can reach from your computer).
In a Claude Code session with the
sccommands, paste it:/sc <url>. Claude finds the page from the slug in the URL and reads it, with the image’s description and transcribed text.Ask about the drawing, or ask for a change. Claude gets the drawing with
drawingand can answer questions, redraw it elsewhere (Mermaid, say) or save a refined version withedit_drawing.
A drawing saved this way is a new version, “drawn by claude”, in the drawing’s History in the PWA; nothing is overwritten, and if you edited the drawing in the meantime Claude’s save is refused. You can restore any earlier version there.
To have a session summary posted automatically before each compaction, install the PreCompact
hook described in Post a summary to scrap when Claude Code compacts.
Permissions and security#
Claude Code asks before using an MCP tool unless you allow it, and the sc commands allow none. You
might allow the read tools (mcp__scrap__search, read, tags) and let
write, edit_meta and the rest keep asking.
To keep Claude off the text of your pages, set SCRAP_MCP_CONTENT_EDIT=0 on the server
(edit_page, edit_drawing, trash_pages and undelete are then not offered, and revert refuses to_rev; add_svg and the upload are governed by SCRAP_MCP_UPLOAD alone), or deny edit_page in one
client’s settings: "permissions": {"deny": ["mcp__scrap__edit_page"]}. A client
setting cannot deny only the text mode of revert, which also undoes kinds and tags.
To stop MCP clients adding images and files at all, set SCRAP_MCP_UPLOAD=0 (add_svg is
not offered and /mcp/pages/<key>/captures is not served).
scrap has no login: any device on the tailnet that can reach the port can use /mcp, so
restrict that with the tailnet ACL (see Deploy on a single server). Off the tailnet, /mcp
can require a bearer token (SCRAP_MCP_TOKEN, ADR 0023), behind Cloudflare Access on
Kubernetes.
Mount scrap only in sessions where reading untrusted text is acceptable: pages may hold injected instructions, and a session that runs with bypassed permissions could be steered by them.