Skip to content

docs(cli): a seat you attach yourself is private; say what it reaches - #2356

Merged
lilyshen0722 merged 2 commits into
mainfrom
docs/private-seat-blast-radius
Oct 20, 2026
Merged

lilyshen0722 merged 2 commits into
mainfrom
docs/private-seat-blast-radius

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Docs only. This says plainly which seats are private, and what a private seat can reach.

Measured 2026-10-20 (non-admin, CLI 0.1.108/0.1.112):

  • Every seat the web app or the daemon sets up is public: the record is bootstrapped with sandbox: { trust: 'public' }.
    • A public Claude seat got an isolated HOME plus --setting-sources '' and --strict-mcp-config. It saw none of the operator's 3 user MCP servers or 10 user skills.
    • A public Codex seat got --ignore-user-config, and the user's server never started.
  • A seat attached with commonly agent attach is private (buildDefaultEnvironment declares no sandbox). That covers attach with no --env, and an env file without a sandbox block.
    • A private Claude seat saw the person's MCP servers, the account's claude.ai connectors and their skills. It ran the skill, and it called a user tool once permissions.allow named it.
    • A private Codex seat started the person's server from config.toml. It runs --dangerously-bypass-approvals-and-sandbox.

Changes:

  • docs/agents/LOCAL_CLI_WRAPPER.md: a new section, "Public and private seats". It states what a private seat reaches (your whole machine as your user), who can instruct it (anyone who can @mention it in the pod, including later invitees), where attach refuses it, and the permissions.allow line Claude needs for your own tools.
  • README.md: the manual foreground path (agent attach + agent run) gets one sentence pointing at that section.
  • docs/cli/opencode.md:

docReferences, docsSiteFacts, scoutKnowledge, runtimeClaims and readmeFrames pass locally.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG

…d what that reaches

Every seat the web app or the daemon sets up is public (sandboxed, none
of your own tools). A seat attached with `commonly agent attach` with no
--env, or with an env file that declares no sandbox, is private: it runs
as you with no sandbox, reaches your whole machine (files, shell tools,
your own MCP servers and their logged-in sessions, skills, account
connectors; Codex without approvals), and takes instructions from anyone
who can @mention it in the pod. The wrapper doc now says so in a section
of its own, and the README's manual path and the OpenCode guide point at
it. Walked 2026-10-20.

Also restores, per Vera's note on #2349, that the DeepSeek model was
walked through an OpenAI-compatible endpoint as well as DeepSeek's API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs gate — #2356 at 5fe01f43da73d4effab2d0a38990f0417c40aaab

One commit, docs only: README.md (+1 sentence), docs/agents/LOCAL_CLI_WRAPPER.md (the new section), docs/cli/opencode.md (pointer + the DeepSeek note). No non-.md path in the diff. Claims checked against origin/main 79600819, which carries #2354 as 8fbbfca6.

What holds

  • (2) A self-attached seat is private. buildDefaultEnvironment (cli/src/commands/agent.js:491) returns { mcp, skills } and declares no sandbox. The only seatBaseline(..., { sandbox: true }) in that file is the bootstrap record at :391; the self-attach path writes the environment without it. An env file with no sandbox block is the same thing to withDefaultSandbox, which treats a missing block, an empty one and mode: 'none' alike.
  • (3) What a private seat reaches. --setting-sources '', --disable-slash-commands, --strict-mcp-config and --permission-mode dontAsk all live inside buildPublicClaudePolicyArgs (adapters/claude.js:251-257), which is spread only on the two public branches — so the private branch loads your settings and MCP servers, and your real HOME. The public seat's own HOME is output.HOME = state.statePath (:209), a per-identity 0700 directory. Codex: the private branch is ['--dangerously-bypass-approvals-and-sandbox'] (adapters/codex.js:432), and the public branch takes --ignore-user-config, which is what makes "a public Codex seat ignores your ~/.codex/config.toml" true.
  • (4) The attach refusal. agent.js:561 — const isPublic = Boolean(pod?.publicRead) || Boolean(pod?.communityListed); — both halves of "a pod anyone can read or one listed in Community", and the gate refuses the seat that declares no enforced sandbox, so narrowing the sentence to a private seat is right.
  • The skills sentence is consistent with #2354 as merged: a public seat doesn't read yours, and does get the ones the seat declares.
  • Both pointers resolve: the heading is ### Public and private seats at LOCAL_CLI_WRAPPER.md:58, so the slug both links use is correct, and ../agents/LOCAL_CLI_WRAPPER.md from docs/cli/ is the right relative path.
  • The DeepSeek note claims no more than the walks: two runs of the same model, both answering an @mention through the Commonly tool, and the "none has been walked yet" sentence is narrowed rather than dropped.

Defect — the section's first sentence is false for the one adapter that cannot be confined

"Every seat that the web app or the daemon sets up is public: its environment declares "sandbox": { "trust": "public" }."

seatBaseline adds that block only when the adapter is in ADAPTERS_WITH_DEFAULT_SANDBOX (cli/src/lib/default-environment.js:46), which is {claude, codex, opencode, qwen}. pi is excluded on purpose — the comment above COMMONLY_DEFAULT_SANDBOX says "A pi seat must never be handed this block: it fails closed on a declared sandbox rather than run under a spec it cannot honour" — and the exclusion is pinned by a test named for it (cli/__tests__/default-environment.test.mjs:40, ['claude','codex','opencode','qwen']).

So a page- or daemon-set-up pi seat gets { mcp: [commonly] } and no sandbox — which is, by this section's own definition two paragraphs later, a private seat. pi is reachable on both paths the sentence names: it is third in BOOTSTRAP_ADAPTER_DETECT_ORDER = ['claude', 'codex', 'pi'] (agent.js:154), so the page's own command picks it when neither claude nor codex is on PATH, --adapter pi selects it directly, and the daemon's derive calls the same seatBaseline (daemon-supervisor.js:199).

Carried to its mint, because a reachability claim needs it: serverAdapterBindingRefusal throws before the record at :391 is built unless the installation already declares runtimeAdapter: 'pi', and adapterForRuntimeBinding (backend/services/grantBrokerConfinement.ts:135) accepts any adapter string, so that declaration exists once an owner has run attach pi for the seat. Narrow, and not hypothetical.

For pi the sentence is not merely inaccurate, it is unachievable: assertNoSandboxDeclared (adapters/pi.js:306) refuses to start a seat whose trust resolves to public — "pi has no enforced sandbox; do not attach a pi seat to a stranger-readable pod". Handing a pi seat the block the sentence promises would stop it booting.

Held on direction, not on pedantry. Every other claim in the section errs toward telling the reader they are exposed; this one tells them they are confined, and it is the sentence a reader checks first — the page says public, so the seat is sandboxed. For a pi seat that is wrong, deliberately and by test. The guide went to an outside user today, which is the reason this is worth a clause rather than a footnote.

Precision note, not a defect

"the listener pre-approves only Commonly's tools" — the pre-approval is mcp__<name>__* for every server declared in env.mcp (adapters/claude.js:637-641), not only commonly. That reads as only-Commonly because the default environment declares only Commonly; a seat whose --env declares the person's own server gets that server's tools pre-approved too, and then the permissions.allow step is unnecessary. The instruction stays correct for the case the paragraph is actually about — servers that live in the person's own config rather than the seat's env spec — so this is wording, and it errs toward an extra step rather than a missing one.

Written by Vera, a Commonly agent — https://commonly.me/pods/6a8f6dc7a1dccf2e02f31015

GATE: HOLD

…s declared servers

Vera on #2356: "every seat the web app or the daemon sets up is public"
is false for pi, which seatBaseline never hands the sandbox block
(ADAPTERS_WITH_DEFAULT_SANDBOX excludes it, and adapters/pi.js refuses a
public trust). The sentence now names the four adapters that get it and
says a pi seat is always private. Also: the listener pre-approves the
tools of every server in the seat's environment, by default only
Commonly's; tools from the person's own ~/.claude.json are the ones that
need permissions.allow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-gate — #2356 at 329ef9aa53c6577ba332e264d69816d2dde7394f

My HOLD at 5fe01f43 is an ancestor; one commit since, docs/agents/LOCAL_CLI_WRAPPER.md only, +2 −2. README.md and docs/cli/opencode.md are blob-identical to the head I held, so everything I verified there carries forward untouched.

The held defect is closed, and the replacement matches the code

"A Claude Code, Codex, OpenCode or Qwen Code seat that the web app or the daemon sets up is public … A pi seat is the exception. pi can't be sandboxed, so a pi seat never gets that block and is always private, however it was set up."

The four named adapters are exactly ADAPTERS_WITH_DEFAULT_SANDBOX (cli/src/lib/default-environment.js:46), the set seatBaseline gates the public block on, and the set cli/__tests__/default-environment.test.mjs:40 pins by name. pi's exclusion is the deliberate one that comment describes, and assertNoSandboxDeclared (adapters/pi.js:306) is why it cannot be reversed.

I checked the whole registry rather than only the adapter I named: it is {stub, claude, codex, pi, opencode, qwen}, so stub is the one other member outside the public set. Leaving it out of a user-facing doc is right — stub.detect() returns (builtin) and stub.spawn() returns a canned string, so a stub seat has no sandbox and no capability; there is nothing for a reader to be exposed by.

The precision note is closed too

"the listener pre-approves the tools of the servers declared in the seat's environment (by default, only Commonly's). Tools from servers in your own ~/.claude.json need a permissions.allow entry in your ~/.claude/settings.json…"

Three claims, three checks. The pre-approval is mcp__<name>__* built from env.mcp (adapters/claude.js:637-641). "By default, only Commonly's" is exact: defaultMcpServers returns [commonlyMcpServer()] and nothing else. And ~/.claude.json is the right file to name for the person's own servers, corroborated twice inside this repo — adapters/claude.js:173 reads join(homedir(), '.claude.json') as operatorConfigPath, and docs/MCP_INTEGRATION.md:172,181 documents it as where global MCP config lives. The remedy still points at ~/.claude/settings.json, which is where permissions.allow belongs and which a private seat loads, because the private branch passes no --setting-sources ''.

Nothing pinned the old sentence: no test under backend/__tests__, cli/__tests__, frontend/src or scripts references LOCAL_CLI_WRAPPER, so the rewrite left no stale fixture behind.

One nuance, not a defect

"always private, however it was set up" is the right claim for any pi seat that runs, and it is worth knowing why it is not simply "unsandboxed by default": an operator who hand-writes an env with sandbox.trust: public or a non-none mode for pi does not get a public pi seat, they get one that refuses to start — assertNoSandboxDeclared throws before the spawn. So the sentence's two readings ("pi is never confined" and "declaring confinement for pi achieves nothing") are both true, and the failure mode a reader could hit is a seat that will not boot rather than one that silently runs unconfined. That errs toward caution, which is the direction this section should err in.

Verdict

Both items I held on are fixed against the code they describe, the fix touches two lines and nothing else moved, and the rest of the PR is the text I verified at the previous head. No defect.

Written by Vera, a Commonly agent — https://commonly.me/pods/6a8f6dc7a1dccf2e02f31015

GATE: PASS

@lilyshen0722
lilyshen0722 added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit cd37b32 Oct 20, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant