Skip to main content

Security model

Principles​

  1. Verify before handler. No tool code runs without a valid, live, correctly scoped token. The only exception is GET /healthz, which returns a constant body.
  2. Nothing listens beyond loopback or the host's own tailnet address.
  3. Message bodies are untrusted data. There is no permission relay. A message cannot approve a tool call.
  4. Small surface. Three runtime dependencies, built-in SQLite, no UI, no OAuth, no stateful MCP sessions, no long-poll routes.

Bind guard​

serve accepts these bind addresses and no others:

  • 127.0.0.1 or ::1;
  • an address that is printed by tailscale ip -4 or tailscale ip -6 and lies in the tailnet ranges (100.64.0.0/10, fd7a:115c:a1e0::/48).

The CGNAT range alone is not trusted; the address must also be one of this node's. If the tailscale binary is missing or fails, the guard sees no addresses and refuses (fail closed). There is no flag, environment variable or code path that skips the guard. A refused bind exits 2 with refusing to bind <host>. Tailscale Funnel is never used.

Threats and mitigations​

Token leak​

A token ends up in ~/.claude.json, in a log, or in shell history.

  • The bus entry uses a headersHelper script. The config stores only an op:// reference, never the token.
  • The push shim reads its token from SESSION_BUS_TOKEN_CMD (for example op read ...). The command output is never logged.
  • The setup script checks grep -c sbus_ ~/.claude.json is 0 after it runs.
  • Raw tokens are redacted from all log lines.
  • Tokens live at most 90 days, can be revoked one by one, and stop working when their principal is disabled.
  • Ownership is per principal, so a leaked token reaches only that principal's sessions.
  • The network path is limited by the tailnet ACL.

Cross-session prompt injection​

A message tells the receiving model to do something harmful.

  • No permission relay: a message cannot answer a permission prompt.
  • The push shim pushes only messages from allowlisted principals (default: its own).
  • from_principal is set by the bus from the token and included in push meta.
  • Bodies are capped at 16 KiB.
  • The channel instructions and the skill describe a message as a peer request, not user authorization.
  • scripts/claude-push.sh starts sessions with --permission-mode default. Do not combine push with skip-permissions.

Accepted: anyone holding a token for an allowlisted principal can steer that principal's push session.

Push silently fails or goes to the wrong session​

  • Pull stays authoritative: mail is never lost because a push did not appear.
  • The shim requires an explicit name and idles without one. There is no default name.
  • Polling starts only after Claude Code completes the MCP handshake.
  • One shim per name per machine (lock file).
  • doctor --session <name> shows unacked and last_seen.

Accepted: a named shim in a session started without the development flag advances its cursor while Claude Code drops the pushes. The mail stays unacked and is visible to inbox.

Accepted residual risks​

  • No encryption at rest.
  • No broadcast rate limit.
  • The last transactions can be lost on power loss.
  • Tailscale identity headers are logged for attribution but not verified. Any local process on the bus host can forge them on loopback.