Skip to content

fix(cli): a public Claude seat gets its declared skills as prompt text - #2354

Merged
lilyshen0722 merged 3 commits into
mainfrom
fix/public-claude-skill-text
Oct 20, 2026
Merged

lilyshen0722 merged 3 commits into
mainfrom
fix/public-claude-skill-text

Conversation

@lilyshen0722

@lilyshen0722 lilyshen0722 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Every seat set up from the Bring your own page is public (COMMONLY_DEFAULT_SANDBOX). A public Claude seat has been running without the Commonly skill that the CLI mounts for it. This PR keeps the public flags and delivers the skill's text another way.

What was wrong

A public Claude spawn passes --disable-slash-commands, which Claude's help defines as "Disable all skills", and --setting-sources ''. Both came in with #754. Later, buildDefaultEnvironment began mounting skills/commonly into every seat's workspace. The two never met: a public seat mounts the skill and can't load it. The MCP server sends no instructions either, so the house rules reached a public seat only if the model happened to call commonly_get_started.

Measured 2026-10-19/20 on live, non-admin, CLI 0.1.108:

Run Result
Page-path seat (trust: public), asked whether it has the commonly skill "No. I don't see a skill named "commonly", or any skills list or Skill tool in my runtime."
Same workspace, claude -p … --strict-mcp-config --setting-sources '' "No. None of the skills available to me in this session is named "commonly"."
Same workspace, --setting-sources project "Yes. The first eight words of its description are: "You are a member of a Commonly workspace""

The fix

The flags stay. Turning skills back on would also give a public seat Claude's built-in skills; a private seat on this Mac lists computer-use and the browser skills. It would also let pod text invoke a slash command.

Instead, claude.js reads each declared skill's SKILL.md, strips the frontmatter, and passes the bodies as --append-system-prompt:

  • on public spawns, both Seatbelt and bwrap, and on the fresh-session retry;
  • inline, so nothing new has to be admitted through Seatbelt;
  • capped at 96 KiB in total, because Linux caps one argv string at 128 KiB. A skill over the cap is skipped with a warning.

The text comes only from the seat's own declared skills.claude sources, never from pod content.

Private seats are unchanged and still load skills natively. This is CLI 0.1.112. 0.1.109 to 0.1.111 are claimed by the older open #2320, #2321 and #2326, and the version check holds them.

Proof

New tests in cli/__tests__/adapters.claude.environment.test.mjs:

  • A public seat gets the body without its frontmatter, and --disable-slash-commands and --setting-sources '' stay.
  • A private seat gets no appended prompt.
  • A public seat with no skills gets none.
  • A missing SKILL.md is skipped, not fatal.
  • The fresh-session retry carries the same text.

Mutation check: removing the two ...(ctx.publicSkillArgs || []) lines fails the body and retry arms. Locally, adapters.claude*, default-environment and the seatbelt* suites run 115 pass and 1 Linux-only skip; eslint src is clean.

End to end on live, before publish. I ran a page-path seat (non-admin, trust: public) from this branch's CLI. Its live argv carried --append-system-prompt, --disable-slash-commands, --setting-sources and --strict-mcp-config.

  • Asked about the skill: "Yes. Its first heading is "# Being a good Commonly member". It arrived as text in my instructions. I don't have a Skill tool or a skills list."
  • Asked a normal @mention question: one reply ("144. This pod is where the operator checks the Bring-your-own Claude Code path…"), posted via the tool, with no double-post and no NO_REPLY.

Ship plan (Cofounder's carve-out from the CLI freeze)

  1. Exact-head PASS, then press by Tue 10-21 14:00 PT. The merge publishes 0.1.112 through npm-publish.yml (OIDC).
  2. Mac smoke of the published package, then by 16:30 PT a non-admin page-path walk on it. The walk asks one normal question and one @mention question, and each must be answered once, with no double-post and no NO_REPLY.
  3. Rollback: no one here can run npm dist-tag, so the rollback ships the way the fix does. Right after the merge, a revert PR (0.1.113, the old behaviour) is opened and gated at its exact head. If the walk fails, pressing it publishes the old code as latest, by about 18:00 PT.

The BYO page installs @commonlyai/cli@latest, so no page change is needed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG

lilyshen0722 and others added 2 commits October 19, 2026 20:05
Every self-serve seat is public (COMMONLY_DEFAULT_SANDBOX), and a public
Claude spawn passes --disable-slash-commands ("Disable all skills") and
--setting-sources '' (#754). So the Commonly skill buildDefaultEnvironment
mounts into the seat's workspace never loaded: a Bring your own page seat
answered "I don't see a skill named commonly, or any skills list or Skill
tool" on 2026-10-19, and the same workspace with --setting-sources project
found it.

The flags stay, so built-in skills stay off and no pod text can invoke a
slash command. The declared skills' SKILL.md bodies, frontmatter stripped,
ride as --append-system-prompt on public spawns and on the fresh-session
retry, capped at 96 KiB in total (Linux caps one argv string at 128 KiB).
Private seats keep loading skills natively. CLI 0.1.109.

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.

Gate — #2354 at 15a1dd0288bb419a5a367ae68a1672781542c10c

Base 11920e89c180e87254547fe2fe652ca63bc5fe67 (current origin/main, an ancestor of the head). 4 files, +183 −3; cli/src/lib/adapters/claude.js is purely additive (0 deleted lines), and the 3 deletions are the three 0.1.108 → 0.1.112 version lines in cli/package.json and cli/package-lock.json. That is the strongest single piece of evidence for the second half of the property — no existing configuration line was edited to make room for this one.

The property, checked

A public Claude seat receives the text of its own declared skills. publicSkillPromptArgs is spread at exactly two sites — claude.js:693 inside if (publicNativeSandbox) (Seatbelt) and claude.js:731 inside if (publicBwrapSandbox). Both the first spawnCtx (:889) and the fresh-session retry spawnCtx (:930) pass publicSkillArgs, so both public branches and the retry carry it at this head.

It runs only for public trust. The gate at :864 reads ctx.environment?.sandbox?.trust, and :794 rebuilds ctx with normalizeSandboxTrust(...) before that read, so a legacy internal record resolves to public rather than silently losing its skills. Independently of the gate, both spread sites sit inside the public-sandbox branches, so a private seat has no site to reach — the trust is enforced twice.

Provenance. The only input is mountSkills' return value, which is mounted.push(absSource) over spec?.skills?.claude — the seat's own declared directories, resolved from its environment spec. The function then reads join(source, 'SKILL.md') and nothing else: no pod content, no event payload, no network, no path derived from a message. A missing SKILL.md warns and continues.

Configuration unchanged. buildPublicClaudePolicyArgs and PUBLIC_DENIED_TOOLS are byte-identical across base and head (blobs e38899a773efd6262dfc3220aa7f258a8ebdff6d and 9a68932e040450b1734bd71bd8c5549c309963c0), so --setting-sources '', --disable-slash-commands, --strict-mcp-config, --no-chrome and --permission-mode dontAsk all stay. The new args are appended after that policy, never interleaved with it.

Argv budget. The cap counts section bytes only; with the ~210-byte preamble the single --append-system-prompt argument stays under Linux's 128 KiB per-argument limit. An over-cap skill is skipped with a warning and later smaller skills still inline — deterministic, order-dependent, documented by the warning.

Mutations

One variable each, foreground, worktree restored and tree identity (140cda38…) re-verified between runs.

# mutation result
M1 drop the Seatbelt-branch spread (:693) 2 failed — skill-body arm + retry arm
M2 drop the bwrap-branch spread (:731) survived 32/32
M3 drop publicSkillArgs from the retry spawnCtx 1 failed — retry arm
M4 make the trust gate unconditional survived 32/32
M5 stop stripping frontmatter 1 failed — skill-body arm
M6 remove the 96 KiB cap survived 32/32
M7 set the cap to 0 2 failed — proves the cap branch is reached and wired

Defects

1. The bwrap branch's carry is pinned by nothing. All five new arms force platform = 'darwin', so M2 — deleting :731 alone — is green. The joint measurement in the ask ("removing the two ...(ctx.publicSkillArgs || []) lines fails two of them") is true of the pair and not of either line: M1 alone accounts for both failures. A later edit that drops the Linux carry would ship undetected, which is this PR's own bug reintroduced on the other host class. The harness for it already exists in this file: the pre-existing arm at :710 ("public trust with NO mode resolves to bwrap off darwin, with the same tool policy macOS gets") forces linux, injects _detectBwrap, and ran green in my local run, so the gap is a missing assertion inside an arm that already reaches the branch, not a missing arm. This is also not a macOS artifact — that linux-forcing arm executes on both hosts, and M2 survived anyway.

2. The cap is unexercised. M6 survives, so the line that keeps argv under the per-argument limit is unpinned in either direction. M7 establishes it is live code rather than dead, so this is coverage, not a dead branch.

Noted, not a defect

  • M4's survival is correct behaviour, not a hole. With the gate unconditional a private seat still gets no appended text, because neither spread site exists outside the public branches. The consequence is only that the arm named for the gate ("a private seat … gets no appended prompt") is satisfied by branch placement and would not notice the gate's removal.
  • The skill body lands in argv, so it is readable to a local user via ps -ww. That is the pre-existing shape of this surface — the task prompt, --model and --mcp-config are already in argv, and the token is not — so the change adds volume, not a new class of exposure.
  • Two cli suite failures in my worktree (mention-event-types.contract.test.mjs) are Cannot find module 'ts-node/register/transpile-only' — this worktree has no backend dependencies installed. The PR touches no backend file. Full cli suite otherwise: 1214 passed, 16 skipped; the adapter suite at head is 32 passed, 1 skipped.

Verdict

The property holds at this head: both public branches and the retry carry the text, it is gated on normalized public trust and structurally unreachable for private seats, it comes only from the seat's own declared skill directories, and no existing configuration line moved. The two defects are regression risk rather than a present fault — the carry is correct today on both branches, and losing it would degrade a seat's instructions rather than weaken its confinement. That is why this is a PASS with the defects named, where #2326's vacuous arm was a HOLD: there the unpinned line was the confinement boundary itself.

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

GATE: PASS

Vera on #2354: every new arm forced darwin, so dropping the bwrap branch's
spread alone stayed green, and removing the cap stayed green. A Linux arm
now drives the bwrap branch with a declared skill, and a cap arm declares a
100 KiB skill before a small one: the big one is skipped, the small one
inlines, and the argument stays under 128 KiB. Both mutations now fail.

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 — #2354 at 6b6f2920d91bf33227411a2de67c7e0981bb6b6f

Carry-forward basis: my PASS at 15a1dd02 (review 5580511874) is an ancestor of this head, origin/main 11920e89 still is too, and the one commit since the baseline is 6b6f2920 test(cli): pin the bwrap branch's skill carry and the 96 KiB cap — one file, +42 −0.

cli/src is unchanged, measured rather than asserted: all 56 blobs under cli/src compare equal between 15a1dd02 and this head, the cli/ file sets are identical (no additions or removals), and cli/package.json / cli/package-lock.json are the same blobs (0481bab5…, f9a19d1f…) still at 0.1.112. So everything the baseline verdict established about behaviour — both public branches carrying the args, the normalized-trust gate, provenance from spec.skills.claude only, buildPublicClaudePolicyArgs byte-identical to main — carries forward unchanged, and only the two coverage defects are new work.

Defect 1 — closed

The new arm forces platform = 'linux', injects _detectBwrap, and asserts on the argv after the --. That slice is the real in-namespace command line: wrapArgvWithBwrap returns ['bwrap', ...flags, '--', ...innerArgv] (cli/src/lib/sandbox/bwrap.js) and the adapter returns args: wrapped.slice(1), while every bwrap flag it emits is --word and never a bare --. So "inside the namespace" is what the arm actually measures, not just "somewhere in argv".

Re-measured at this head, each mutation separately, tree 1d02364b… restored and re-verified between runs:

# mutation before (15a1dd02) now
M2 drop the bwrap spread (claude.js:731) survived 32/32 1 failed — the new Linux arm
M6 remove the 96 KiB cap survived 32/32 1 failed — the new cap arm

M2 fails for its reason: appended(inner) is null once :731 is gone, so the inner argv carries no --append-system-prompt at all.

The two branches are now independently witnessed, which is the part that matters. M1 (drop the Seatbelt spread at :693) reddens 3 arms — the skill-body arm, the new cap arm and the retry arm — and leaves the Linux arm green; M2 reddens only the Linux arm. Neither mutation can be satisfied by the other branch's coverage, so this is no longer a joint measurement.

Defect 2 — closed, including the half I did not ask about

M6 fails for its reason, not incidentally: with the cap gone the appended text contains ## Skill: big-skill, and the arm's .not.toContain is what catches it. The 100 KiB fixture does cross the 96 KiB total on the first iteration, so the branch is exercised as intended.

I also mutated the cap's continue to break (M8) — the arm fails, so it pins both halves of its name: the over-cap skill is skipped and the later smaller one still inlines. A break would have silently dropped every skill after an oversized one, and this arm now refuses that.

Fixture hygiene

beforeEach sets darwin and afterEach restores the captured originalPlatform, so the Linux arm's in-body override does not leak into the arms that follow it — the cap arm runs immediately after and needs darwin for the Seatbelt path. Fresh mkdtemp per test, and big-skill is created under path.dirname(skillSrc), which afterEach removes, so the arm's fs.mkdirSync(big) cannot collide across runs.

Suite

Adapter suite at this head: 34 passed, 1 skipped — matches the ask. Full cli suite: 1216 passed, 16 skipped, exactly +2 against the baseline's 1214, with the same two pre-existing failures in mention-event-types.contract.test.mjs (Cannot find module 'ts-node/register/transpile-only' — this worktree has no backend dependencies; the PR touches no backend file).

Verdict

Both defects I named are closed by the instrument I would have used to check them, each mutation now reddens an arm that no other branch's coverage can satisfy, and the production path is bit-identical to the head I already passed. No new defect.

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

GATE: PASS

@lilyshen0722
lilyshen0722 marked this pull request as ready for review October 20, 2026 04:06
@lilyshen0722
lilyshen0722 added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit 8fbbfca Oct 20, 2026
22 checks passed
samxu01 pushed a commit that referenced this pull request Oct 21, 2026
…0.1.118)

The Codex adapter never read `environment.skills`, so every self-serve
Codex seat (all public) ran without the Commonly skill buildDefaultEnvironment
declares for it: the Codex half of the gap #2354 closed for Claude.

`codex exec` has no skill loader a public seat can use, but it reads
$CODEX_HOME/AGENTS.md as the user's instructions, and a public seat's
CODEX_HOME is one this CLI owns (preparePublicCodexHome). Measured on
codex-cli 0.162.1 with the public flags (`--ignore-user-config` skips
config.toml only): a marker in that file came back on a new thread, and a
changed marker came back on `exec resume`, so existing seats pick it up on
their next turn.

- codex.js: public spawns write the declared `skills.claude` SKILL.md bodies
  (ADR-008's SKILL.md list; Codex reads the same format) to the public home's
  AGENTS.md, 0600, staged then renamed, capped at 32 KiB; removed when
  nothing is declared. A private seat's home is the operator's and is never
  written.
- skill-prompt.js: the reader #2354 wrote inside claude.js, moved out so both
  adapters render a skill the same way; Claude's output is unchanged.
  environment.js's mountSkills uses the shared path resolver.

Walk on the real binary through the adapter: with the bundled skill
declared, the seat quoted "# Being a good Commonly member" (3.5 s); with
none declared, "NONE".

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