Documentation
Everything about agent-sidecar: how the canvas works, the MCP tools your agent calls, the HTTP API underneath, and how to write artifacts that talk back.
Installation
agent-sidecar is a Claude Code plugin (and a standard MCP server usable from other agents). The repo doubles as its own plugin marketplace, and the server itself ships as a self-contained bundle on npm. The only prerequisite is Node 20+ on your PATH.
# in any Claude Code session
› /plugin marketplace add smarchetti/agent-sidecar
› /plugin install agent-sidecar@agent-sidecar
Restart Claude Code (or /reload-plugins). The plugin launches node ${CLAUDE_PLUGIN_ROOT}/dist/sidecar.js — the bundle ships inside the plugin, so there is no registry fetch and installing works offline. agent-sidecar is then available in every project.
Verify with /mcp: you should see agent-sidecar listed as connected.
Other agents
agent-sidecar is a standard MCP stdio server — the plugin is just Claude Code packaging. Any MCP client can run it with this one entry:
{ "mcpServers": { "agent-sidecar": { "command": "npx", "args": ["-y", "agent-sidecar"] } } }
On a locked-down network, npm i -g agent-sidecar once and use { "command": "agent-sidecar" } to skip the registry on every launch.
| Agent | Where to put it |
|---|---|
| Cursor | .cursor/mcp.json in the project, or ~/.cursor/mcp.json globally |
| VS Code (Copilot) | .vscode/mcp.json — same entry, but under a "servers" key instead of "mcpServers" |
| Codex CLI | ~/.codex/config.toml:[mcp_servers.agent-sidecar] · command = "npx" · args = ["-y", "agent-sidecar"] |
| Gemini CLI | ~/.gemini/settings.json under "mcpServers" |
| Windsurf | ~/.codeium/windsurf/mcp_config.json under "mcpServers" |
Everything transfers: the tools, the canvas, token auth, the .sidecar/ session files. Two Claude-flavored details to be aware of:
- The helper injected into artifacts is named
claude.send()regardless of which agent is driving. - The background-watcher pattern needs an agent that can run shell commands in the background and surface their output. The blocking
await_interactiontool works in every client.
"args": ["agent-sidecar@0.11.0"].Your first artifact
You don't call agent-sidecar directly — your agent does. Ask for something visual:
› show me three layout options for the settings screen on the canvas
The agent creates an artifact (a complete HTML document), your browser opens the canvas, and the artifact renders live. When the artifact contains buttons wired to claude.send(...), clicking one delivers the payload back into the conversation, and the agent continues with your choice.
That's the whole product: the agent shows, you click, the agent continues.
Architecture
Two halves: a thin MCP server on stdio per agent session (facing your agent) and one canvas server per machine on localhost (facing your browser). The first session starts the canvas server as a detached process; every session after that attaches to the same one.
The interesting part is the return path. A click in the browser POSTs to the webhook and lands in the interaction queue of the session that owns that artifact. The agent consumes its own queue in one of two ways (below) — in both cases the payload arrives as ordinary tool/command output, which is why agent-sidecar works without any push mechanism (like Claude Code channels) being available.
Artifacts render inside a sandboxed iframe with an opaque origin. Artifact code can't reach the canvas shell, the auth token, or the server; its only output channel is claude.send(), which crosses a postMessage bridge that the canvas shell validates and forwards with the token.
Receiving input: two patterns
1. Blocking tool — quick decisions
The agent calls await_interaction right after showing choices. The tool call parks until you click (or times out after up to 120s, in which case the agent simply calls it again). Best when a response is expected within seconds — the answer lands in the same turn.
2. Background watcher — long waits
The agent starts a background shell task against the long-poll endpoint and keeps working. When you eventually click, the watcher exits and the agent is re-invoked with the payload:
curl -s "http://127.0.0.1:<port>/api/wait?token=<token>&session=<id>&artifact_id=<id>"
agent-sidecar's server instructions teach the agent both patterns each session — with the real port, token, and session id baked in — plus the rule of thumb: block for quick picks, watch in the background when the user may take minutes.
Sessions & files
Every agent session — in any project, from any MCP client — appears as its own session on one canvas. The sidebar is a tree — repo, then worktree, then session (labelled with its git branch), then that session's artifacts nested beneath it — so the whole canvas is visible at once and any artifact is one click away; collapse any level you don't need. Artifacts and interaction queues are per session, so parallel sessions never read each other's answers.
The canvas follows your system light/dark preference, and the sun/moon control in the top bar pins a choice that's remembered per browser. Artifacts stay light in both themes — an artifact is the agent's own document, not part of the shell.
The stage has two modes, toggled in the top bar or with t: single shows one artifact at a time, timeline stacks every artifact of the session in one scroll for reviewing a whole session at once. Timeline cards size themselves to their content — the injected helper reports its own height, since a sandboxed frame can't be measured from the outside — and buttons work in every card, each attributed to its own artifact.
Grouping follows your checkouts rather than directory names: sessions in two git worktree checkouts of one repo nest under a single repo, keyed on the main worktree's git dir and named from the origin remote when there is one. The worktree level appears only when a repo has more than one checkout; a non-repo directory gets its own group, marked dir.
A session that ends stays on the canvas, dimmed, so you can still read what it produced — dismiss it with the × on its row. When an artifact arrives in a session you aren't looking at, that session is badged and a clickable toast appears; the view never jumps out from under an interaction you're in the middle of.
The server prefers port 8765 (override with SIDECAR_PORT) and scans upward if a foreign process holds it. It outlives your agent sessions and exits by itself after 30 minutes with no sessions and no canvas tab (SIDECAR_IDLE_EXIT_MS, 0 disables). Manage it directly:
npx agent-sidecar --status # server, sessions, artifact counts
npx agent-sidecar --stop # shut it down
npx agent-sidecar --serve # run in the foreground (debugging)
Machine-wide state lives in ~/.agent-sidecar/ (override with SIDECAR_HOME):
| File | Contents |
|---|---|
server.json | Server coordinates: { pid, port, url, token, startedAt }, mode 0600. Kept (marked stoppedAt) when the server exits, so the next one reuses the token and old watcher URLs stay valid. |
state.json | Every session's canvas contents, restored on restart so a server bounce — or a reboot — doesn't lose your artifacts. |
server.log | Server output. The server runs detached, so this is where its errors go. |
Per-project state stays in .sidecar/ under the directory your agent runs in (add it to .gitignore):
| File | Contents |
|---|---|
session.json | How external callers reach this project's session: { pid, serverPid, port, url, token, sessionId, startedAt }. Removed on clean exit. |
interactions.jsonl | Append-only log of every interaction for this project (one JSON object per line, with seq, receivedAt, kind, sessionId, artifactId, payload). Rotates to .old at 5 MB. tail -f it to watch the loop live. |
MCP tools
Six tools, prefixed mcp__plugin_agent-sidecar_agent-sidecar__ when installed as a plugin.
create_artifact
| Param | Type | Description |
|---|---|---|
title | string, required | Shown in the canvas sidebar |
html | string, required | Complete, self-contained HTML document |
open | boolean | Open the browser if no canvas tab is connected (default true) |
Returns the artifact id and canvas URL. New artifacts take focus on connected tabs automatically.
update_artifact
| Param | Type | Description |
|---|---|---|
id | string, required | Id from create_artifact |
html | string, required | Replacement HTML document |
title | string | New title (optional) |
Connected tabs hot-reload the artifact. Prefer updating over creating a new artifact when iterating on feedback — it keeps the sidebar clean and the user's focus in place.
await_interaction
| Param | Type | Description |
|---|---|---|
artifact_id | string | Only accept interactions from this artifact; others stay queued |
timeout_seconds | number | How long to block (default 25, max 120) |
Returns the oldest matching queued interaction immediately if one exists, otherwise blocks. On success:
status=received
[2026-07-05T00:04:22.597Z] from artifact a1-xq5zj ("Plugin install check"):
{"install":"confirmed"}
On timeout it returns status=no_response — the caller just calls again to keep waiting.
get_interactions
No parameters. Drains all queued interactions without blocking — useful for catching clicks that happened while the agent was doing other work.
list_artifacts / remove_artifact
list_artifacts lists id, title, and timestamps for everything on the canvas. remove_artifact takes an id and removes it; connected tabs move to the newest remaining artifact.
HTTP API
Bound to 127.0.0.1 only. Every /api/* endpoint requires the server token — via the X-Sidecar-Token header, a ?token= query param, or Authorization: Bearer.
| Endpoint | Auth | Description |
|---|---|---|
GET / | — | The canvas UI (all sessions) |
GET /events | — | SSE stream: a snapshot of every session on connect, then session / session_removed and created / updated / removed (each carrying a sessionId) |
GET /artifact/:id | — | Artifact HTML with the claude.send() helper injected |
POST /api/webhook | token | Queue a payload for an agent. JSON body { artifactId, payload } is recorded as an artifact interaction and routed to that artifact's session; anything else is recorded as an external webhook event, body forwarded as-is. Routing without an artifact: ?session=, else the session the canvas is showing, else the most recently active one |
GET /api/wait | token | Long-poll: returns the next interaction as JSON. ?session= picks the session (an unknown id is a 404, never another session's queue); ?artifact_id= filters; ?timeout=SECS caps the wait (then 408); no timeout means wait indefinitely |
GET /api/drain | token | Returns and clears everything queued for ?session= without blocking |
GET /api/sessions | token | Every session with its label, liveness, and artifacts |
POST /api/shutdown | token | Stop the server (what --stop calls) |
POST /api/restart | token | Hand the canvas to the newest version any session has registered from, reusing the same port. 409 when nothing newer is attached. This is what the canvas's update bar calls |
GET /health | — | { ok, server, version, protocol, pid, port, idle, sessions, artifacts, canvasTabs, queuedInteractions } |
claude.send()
Every artifact gets a claude global injected before its own scripts run. One method:
claude.send(payload) // → Promise<boolean>
payloadis any JSON-serializable value; it arrives in the agent's context verbatim.- Resolves
trueonce the canvas shell confirms delivery; the shell also shows a "✓ Sent" toast. - Resolves
falseif delivery fails or times out (5s), or if the identical payload was sent within the last 1.5s — the built-in double-click debounce.
Under the hood the sandboxed artifact posts a message to the canvas shell, which attaches the session token and forwards to /api/webhook. Artifact code never handles the token.
Writing artifacts
Artifacts are complete HTML documents rendered in a sandboxed iframe (allow-scripts allow-forms allow-popups, opaque origin). Practical rules:
- Inline everything. The opaque origin means same-origin API calls are blocked and cross-origin requests fail CORS. No CDNs, no external fonts — inline CSS and JS only.
- No storage.
localStorage/cookiesare unavailable. Keep state in JS variables; if it must survive, send it to the agent. claude.send()is the only output. Design every artifact around the payload you want back.
Pattern: choice cards
<button onclick="claude.send({ choice: 'option-a' })">Choose A</button>
<button onclick="claude.send({ choice: 'option-b' })">Choose B</button>
Pattern: form submission
<form onsubmit="event.preventDefault();
claude.send(Object.fromEntries(new FormData(event.target)))">
<input name="title" /> <textarea name="notes"></textarea>
<button>Send</button>
</form>
Pattern: approve / iterate bar
claude.send({ verdict: 'approve' })
claude.send({ verdict: 'iterate', notes: field.value })
Give the user visual confirmation on top of the toast — flip the clicked button's label, highlight the selected card — and structure payloads as small objects ({ choice, notes }) rather than bare strings, so the receiving side stays unambiguous.
External webhooks
Anything that can POST can push events into a session — CI, scripts, other tools. Read the coordinates from .sidecar/session.json (or ~/.agent-sidecar/server.json):
url=$(jq -r .url .sidecar/session.json)
token=$(jq -r .token .sidecar/session.json)
sid=$(jq -r .sessionId .sidecar/session.json) # omit to land on the session you're viewing
curl -X POST -H "X-Sidecar-Token: $token" \
-d "build failed on main: https://ci.example.com/run/1234" \
"$url/api/webhook?session=$sid"
Non-artifact payloads are tagged kind: "webhook" and reach the agent through the same queue — a waiting await_interaction (without an artifact filter) or /api/wait picks them up.
Security model
- Localhost only. The HTTP server binds
127.0.0.1; nothing off the machine can reach it. Don't port-forward or tunnel it — anything that reaches the webhook is eventually placed in front of the agent. - Token on every API call. Without auth, any webpage you visit could fire cross-site POSTs at localhost and inject text into your session. All
/api/*endpoints require a random 128-bit token, stored mode0600in~/.agent-sidecar/server.json. Web pages can't read it (it lives in that file and the canvas shell); local processes can, and are trusted anyway. - Sessions are isolated from each other. An interaction is delivered to the session that owns the clicked artifact, and a watcher that names a session is never handed a different one's queue — so a second agent can't consume the answer meant for the first.
- Sandboxed artifacts. Artifact HTML is authored by the agent, which makes it semi-trusted — a prompt-injected artifact shouldn't get free rein. The opaque-origin sandbox denies it the token, the canvas DOM, storage, and same-origin network; the canvas shell only accepts messages from the artifact iframe it created.
- Interactions are user input. agent-sidecar instructs the agent to treat payloads as coming from you. Keep that in mind if you point external systems at the webhook: whatever they post is read by the agent with that framing.
Development
git clone https://github.com/smarchetti/agent-sidecar
cd agent-sidecar
bun install
bun test # 55 end-to-end tests over real MCP stdio
bun run test:node # the same suite against the built bundle under node
Source lives in src/: sidecar.ts (CLI entry), server.ts (the singleton canvas server), client.ts (server discovery and the session link), mcp.ts (the tools), shared.ts (paths and types), and canvas.html (the browser shell, inlined into the bundle at build time). Tests run against an isolated server via SIDECAR_HOME and a random port, so they never touch the one you're using. To run your working copy in a live session:
claude --mcp-config dev.mcp.json
--stop the server after switching versions.Releasing: bump version in package.json and the agent-sidecar@<version> pin in .claude-plugin/plugin.json, then npm publish (the prepublishOnly hook rebuilds dist/sidecar.js). Publish before pushing the manifest, so it never points at a version that doesn't exist.
Troubleshooting
| Symptom | Check |
|---|---|
| Plugin doesn't load | /mcp shows agent-sidecar? Is bun on PATH? First launch fetches from npm — needs network once. |
| Canvas didn't open | The URL is in every create_artifact result and in .sidecar/session.json. Auto-open only fires when no tab is connected — one tab covers every session. |
| Clicks not arriving | tail -f .sidecar/interactions.jsonl — if clicks appear there, the agent just hasn't consumed them yet (get_interactions drains). If not, check --status and the browser console. |
| Can't see your session | npx agent-sidecar --status lists every session. Sessions are labelled by git branch, grouped by project. |
| Wrong port | A foreign process holds 8765, so the server scanned upward. server.json and session.json always have the live URL. |
| 403 from the API | Missing/stale token — re-read server.json. The token survives server restarts but not a deleted ~/.agent-sidecar/. |
| Server won't die | npx agent-sidecar --stop. It also self-exits after 30 idle minutes; ~/.agent-sidecar/server.log has its output. |
| Old version running | Plugin pins agent-sidecar@<version>. Update the plugin, restart Claude Code. |