Skip to content

fix(cli): public Codex seats get their skills; the skill closes handoffs with an @mention (0.1.118) - #2368

Merged
lilyshen0722 merged 4 commits into
mainfrom
feat/codex-public-skill
Oct 21, 2026
Merged

lilyshen0722 merged 4 commits into
mainfrom
feat/codex-public-skill

Conversation

@lilyshen0722

@lilyshen0722 lilyshen0722 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Why

The Codex adapter never read environment.skills. Every self-serve Codex seat is public (COMMONLY_DEFAULT_SANDBOX), so every outsider's Codex has run without the Commonly skill that buildDefaultEnvironment declares for it. This is the Codex half of the gap #2354 closed for Claude on 10-20. Cofounder put it first in the post-call CLI release, ahead of the SKILL.md delegator line.

What

cli/src/lib/adapters/codex.js. A public spawn writes the declared skills' SKILL.md bodies, frontmatter stripped, to the seat's own $CODEX_HOME/AGENTS.md.

  • Why this file: codex exec has no skill loader a public seat can use, but it reads that file as the user's instructions. The public home is one this CLI owns (preparePublicCodexHome), so nothing of the operator's ~/.codex/AGENTS.md is involved.
  • Write: mode 0600, staged then renamed, capped at 32 KiB (Codex's default project_doc_max_bytes).
  • Removed when nothing is declared, so a dropped skill stops applying.
  • Private seats run on the operator's home, which is never written.

The key, skills.claude. It is ADR-008's list of SKILL.md-format skills ("drivers copy or symlink as appropriate"), and Codex reads the same format. So there's no new key, and existing seat records work as they are.

cli/src/lib/skill-prompt.js (new). The reader #2354 wrote inside claude.js, moved out so both adapters render a skill the same way; a copy would drift. Claude's --append-system-prompt text is byte-for-byte unchanged. mountSkills uses the shared path resolver.

The skill (docs/agents/skills/commonly/SKILL.md, mirrored to cli/skills/commonly/SKILL.md) gains "Close every handoff with an @mention". In the C14 call rehearsal, Claude handed Codex a change and promised a review. Codex posted the result without @mentioning Claude, so the review never came.

Version 0.1.118. Claimed so far:

Facts measured before building (codex-cli 0.162.1, this adapter's public flags)

  1. A new thread reads $CODEX_HOME/AGENTS.md under --ignore-user-config, which skips config.toml only. A marker in the file came back verbatim.
  2. exec resume re-reads it. The marker changed between turns and the resumed thread answered the new one, so live seats pick the skill up on their next turn, not only on a new thread.
  3. Codex's own $CODEX_HOME/skills/ loader exists, but it loads a body only when the skill is used. House rules have to be present every turn, so this uses AGENTS.md.

Proof

Walk on the real binary, through the adapter's public path: codex.spawn with sandbox.trust: public, a scratch public home and the bundled cli/skills/commonly.

run asked for the skill's first heading time
skill declared "# Being a good Commonly member" (exact) 3.5 s
nothing declared (control) "NONE" 3.7 s
skill declared, asked what a handed-work result must include "Your result post must @mention the agent who handed you the work so they're notified." 4.1 s

AGENTS.md was written at 0600, 11,449 bytes.

Tests: adapters.codex.skills.test.mjs, 6 arms:

  • the body is in the public home's AGENTS.md before codex starts, without frontmatter, and nothing of the operator's leaks in;
  • the file is 0600;
  • a relative path resolves from the workspace;
  • a dropped skill stops applying;
  • a missing SKILL.md is skipped, not fatal;
  • a private seat's operator home is never written.

Mutations, each failing at least one arm:

mutation result
never written 5 failed
stale file kept 1 failed
world-readable 1 failed
relative path from process cwd 1 failed
private seats written too 1 failed
frontmatter kept (shared reader) 2 failed: the Codex arm and #2354's Claude arm

Full CLI suite: 1,222 passed, 16 skipped. The one local failure needed backend/node_modules in the worktree; after linking it, 3 of 3 passed. Lint is clean.

Merge

Draft. This goes in the post-call CLI release, after Wednesday's first deploy. Merging auto-publishes via OIDC, so after the merge:

  1. a Mac smoke on @latest;
  2. a page-path walk with a fresh Codex seat, asking for the skill's heading, as was done for 0.1.112.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG

@lilyshen0722 lilyshen0722 changed the title fix(cli): a public Codex seat gets its declared skills as AGENTS.md (0.1.118) fix(cli): public Codex seats get their skills; the skill closes handoffs with an @mention (0.1.118) Oct 20, 2026

@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 — #2368 at 0422a9aaf323de25d247c4926dddfdb6180cbe18

Live head resolved first and equals the asked head. origin/main cd37b32e is an ancestor; 2 commits, 10 files, +282 −41.

The refactor is byte-identical, measured rather than read

The claim that carries the most risk here is that moving #2354's reader into skill-prompt.js left Claude's --append-system-prompt text unchanged. Reading the diff says yes; I measured it instead. Two worktrees — cd37b32e and this head — the real claude.spawn driven with the same fixture (two declared skills, one carrying a frontmatter description, one not), capturing the actual argv value and hashing it:

old (cd37b32e)  len=260 sha256=3e9ee7b5260c26f3778bb3e220c698ce12c50dd6ee7f71439c2348c0b279b4b0
new (0422a9aa)  len=260 sha256=3e9ee7b5260c26f3778bb3e220c698ce12c50dd6ee7f71439c2348c0b279b4b0

Identical, including the preamble's placement: the old code appended …loaded skill.\n\n inside the literal, the new one adds \n\n in the template around SKILLS_AS_TEXT_PREAMBLE, and the result is the same string.

mountSkills's resolution is also unchanged, including the degenerate inputs. expandHome returned a non-string untouched and the old call site then ran the same isAbsolute, so a non-string threw there before and throws in resolveSkillSource now; '' resolved to pathResolve(base, '') both ways. The two differ only in where the type guard sits, not in what any input resolves to.

The Codex half

writePublicCodexInstructions is called inside if (publicSandboxMode !== null), on the line after preparePublicCodexHome, and nowhere else. It writes <public home>/AGENTS.md staged-then-renamed at 0600, removes the file when nothing is declared, and reads only ctx.environment?.skills?.claude. The public home is per-seat — sha256(agentName || cwd || 'anonymous') under ~/.commonly/codex-homes/ — and preparePublicCodexHome already refuses when that path resolves to the operator's CODEX_HOME, so two seats with different declarations cannot share an AGENTS.md.

The arm I most wanted exists and is the right shape: "a private seat runs on the operator's own home, which is never written" asserts both that CODEX_HOME is the operator's and that their AGENTS.md still holds its original content and that the public one is ENOENT.

Both skill copies

The new paragraph is pinned twice over: the new arm asserts it in docs/agents/skills/commonly/SKILL.md, and the pre-existing byte-identity arm — the one carrying the #702 incident where 0.1.4 shipped without a guardrail added to the packaged copy only — propagates any DOCS edit to cli/skills/commonly/SKILL.md. Both blobs are aae7e0ac… at this head, so they are identical in fact and the identity is enforced, not assumed.

Mutations

Six, one variable each, tree identity (38298181) verified after every restore.

# mutation result
C1 write for every seat, not only public fails — the private-seat arm
C2 no removal when nothing is declared fails — the dropped-skill arm
C3 mode 0644 instead of 0600 fails — "the file is private to the operator"
C6 pass the raw declaration, skipping resolveSkillSource fails — the relative-path arm
C4 write straight to the target, no stage-then-rename survived
C5 raise the Codex cap to 1 GiB survived

Both survivors were predicted, and neither is a defect:

  • C4 cannot be witnessed in-process. The property is that a concurrent reader — codex itself — never sees a half-written file, and a single-threaded arm has no way to observe it. A correct precaution whose removal nothing catches; the same shape as #2361's credential-guard survivor.
  • C5 is a coverage asymmetry worth naming precisely. The shared reader's cap logic is witnessed, by the Claude suite's 96 KiB arm, which still passes at this head. The Codex cap's value is not: raise Claude's cap and an arm reddens, raise Codex's and nothing does.

Notes

  • 32 KiB is asserted, not sourced. The comment says it is "Codex's default project_doc_max_bytes for AGENTS.md files"; that string appears nowhere else in our tree, and I could not find it in three plausible upstream doc paths. The direction of risk is one-way: if codex's real default is larger, ours is merely conservative, but if it were smaller, codex would truncate below our own cap and a declared skill would be cut with no warning from either side. The live walk on 0.162.1 proved the file is read on a new thread and re-read on exec resume, and that a heading came back verbatim — with a small skill, so it did not exercise the truncation boundary.
  • The 'anonymous' fallback in the public-home identity hash means two seats with neither agentName nor cwd would share a home. Pre-existing, not this PR's, and attach requires a name.
  • Suites: the four touched suites 90 passed / 1 skipped; full cli 1223 passed, 16 skipped plus this worktree's two known mention-event-types.contract failures (ts-node/register/transpile-only, no backend dependencies here); eslint src clean.

Verdict

A public Codex seat gets its declared skills as AGENTS.md in a home this CLI owns, the operator's home is never written and an arm proves it, the shared reader leaves Claude's text byte-for-byte as it was, and the new rule is pinned in both copies of the skill. No defect.

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

GATE: PASS

lilyshen0722 and others added 3 commits October 21, 2026 10:00
…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
C14 call rehearsal, 2026-10-20: Claude handed Codex a change and promised to
review it; Codex posted the result without @mentioning Claude, so Claude
never woke and the review never came. The kernel now cues the first handoff
(#2367); the skill states the rule for every runtime that loads it.

Edited in docs/agents/skills/commonly/SKILL.md (prepublishOnly copies it over
the packaged one) and mirrored; skill-sync pins it beside the #702 guardrail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wkz5axAzCvoLEFkZ5KtkKG
Vera's C5 on #2368: raising PUBLIC_CODEX_INSTRUCTIONS_MAX_BYTES to 1 GiB
failed no arm, and the 32 KiB figure was asserted without a source.
codex-cli 0.162.1 ships `project_doc_max_bytes = 32768` in its default
config (read from the shipped binary); the comment now says so. The new
arm inlines a section of exactly 32768 bytes and nothing after it, so a
cap one byte lower or any larger cap fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015eTFc9yqm2vKZiUyvLuBT4
@samxu01
samxu01 force-pushed the feat/codex-public-skill branch from 0422a9a to 2f69e41 Compare October 21, 2026 17:01
samxu01 pushed a commit that referenced this pull request Oct 21, 2026
…of a --remotes glob

`--remotes=origin/main` is a git glob that becomes origin/main/*, so it
matched nothing. On a push the scan still worked, because `--branches`
held the checked-out branch, but a pull request checks out a detached
merge commit with no local branch, and every PR scan reported "0 commits
scanned, no leaks found": #2375, #2368 and #2389 all did. That is how
#2213's fixture passed review and only went red on main. Scan HEAD (the PR
merge commit, or the pushed branch) plus origin/main and origin/v1.0.x by name.

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

@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 on #2368 at exact head 2f69e419081ca72677ead0b1ad2dc4bee08c1f30. My PASS at 0422a9aa is the baseline.

GATE: HOLD — one defect, in the delta that was written to close C5, plus one thing the new comment should say rather than imply.

C5 itself is genuinely closed, and the pin is tighter than you claimed. The new arm reddens for a cap one byte lower, a cap one byte higher, and the 1 GiB that survived my first gate — three mutations, each failing the arm that covers it, so the value is pinned to the byte in both directions rather than bounded on one side. And the 32,768 figure holds up away from the binary: the official Codex guide states the default in those words — Codex "stops adding files once the combined size reaches the limit defined by project_doc_max_bytes (32 KiB by default)."

The defect: the cap is applied to the sum of section bytes, not to the file Codex reads. readSkillSections budgets Buffer.byteLength(section) only; skillsAsText then prepends a 165-byte preamble plus \n\n, joins sections with \n\n, and the writer appends a final \n — all after the budget is spent. Measured by running your own fixture through the real skillsAsText: sections exactly at 32,768 produce an AGENTS.md of 32,936 bytes, 168 over the cap. With several skills it is worse by another two bytes per join.

So in exactly the case the new arm certifies as correctly inlined, the artifact crosses the number this commit exists to respect — and the property the comment states, "a skill that would cross it is left out whole rather than cut mid-text", fails by 168 bytes: the documented symptom for crossing the cap is "Instructions truncated", so the tail of that last skill is the part at risk. The fix is small and belongs where the budget is spent: subtract the rendered overhead (preamble + \n\n + joins + trailing newline) before admitting a section, or budget the rendered text instead of the sections. The arm that proves it is the one you already have, with the assertion moved from the section to the written file's byte length.

Second item, not a defect but it decides whether the first one bites hardest. The comment cites project_doc_max_bytes, whose documented behaviour governs project AGENTS.md files discovered from the working directory upward, as a combined budget across them. This file is written to $CODEX_HOME/AGENTS.md — the global instructions file, as the comment two blocks above says. Reading the default out of the shipped binary establishes the number; it does not establish that this limit governs that file. Both readings have a consequence worth writing down:

  • if the global file is inside the same combined budget, then a seat whose workspace carries its own AGENTS.md shares one 32 KiB allowance with ours, so a skill set sized to the cap can crowd out the project's own doc (or be crowded out by it) — and the local cap can only ever be a ceiling, never a guarantee;
  • if it is not, then 32 KiB is a prudent self-imposed limit and the sentence "Codex truncates past it" is an inference rather than a sourced fact.

Say which, in the comment, with the same rigour the version and the figure now get.

The rebase is clean, and I checked it rather than accepted it. Branch base is 7e21f486, which is main's tip, so the branch is up to date. Comparing every touched file's blob between 0422a9aa and this head: eight identical, two changed — the new test file, and codex.js, whose change is the comment alone. cli/package.json and cli/package-lock.json are byte-identical to the pre-rebase head, so the conflict resolution kept 0.1.118 as you said. One small correction to the ask: #2361 did not touch cli/src/lib/environment.js — it touched default-environment.js and the adapter list; main's environment.js carries no dsh content, and the PR's version of that file is unchanged from the pre-rebase head, so nothing of #2361's was reverted.

Suites. The full cli suite at this head: 1,144 passed, 6 suites failing to run. Those six are machine-local, not yours — I ran the same suite at the base 7e21f486 as a control and got the identical six, failing on Cannot find module 'commander' from a borrowed node_modules. Against that control the PR is +8 passing tests and no new failure. The four suites the ask names pass on their own (48 tests). Product tree restored to a2b7ce11 after every mutation.

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

GATE: HOLD

… of sections; say where Codex's own limit applies

Vera's HOLD on #2368 at 2f69e41. The cap counted section bytes only, so the
preamble, the separators and the final newline came after the budget was
spent, and a file the arm certified ran 168 bytes past 32768.
readSkillSections now budgets the text skillsAsText returns, with an optional
trailer, and the Codex writer passes its final newline as that trailer.
Claude's text is unchanged; its 96 KiB budget now counts the preamble too.

The comment is corrected from Codex's source at rust-v0.162.1. The global
$CODEX_HOME/AGENTS.md is read whole (codex-home/src/instructions/mod.rs) and
sits outside project_doc_max_bytes, which budgets only discovered project docs
(core/src/agents_md.rs). So 32 KiB is our own ceiling, Codex does not truncate
this file, and it never shares a budget with a workspace AGENTS.md.

Two arms measure the file one byte either side of 32768. Six mutations each
fail at least one: section-only budget, trailer uncounted, trailer counted
but not written, cap +1, cap -1, cap 1 GiB.

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

@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 on #2368 at exact head 6d065459537f3c7b0f5c886dab214382849e3ecc. My HOLD at 2f69e419 (review 5603646162) is the baseline.

GATE: PASS. Both items are closed, and the second is closed by evidence I could check rather than by assertion.

Item 1 — the cap now bounds the file, and the accounting is exact rather than approximately right. readSkillSections starts total at the preamble plus the trailer and charges each section its own \n\n; the rendered text is preamble + separator + sections joined by separator + trailer. Those two sum to the same thing for any n: budget = |preamble| + |trailer| + Σ|section| + 2n, rendered = |preamble| + 2 + Σ|section| + 2(n−1) + |trailer|. Equal, so admission at total ≤ maxBytes makes the file at most the cap by construction, not by a margin.

Measured rather than derived. Running the real skillsAsText: a single skill sized to fit produces a file of exactly 32,768 bytes (where the same input at the previous head gave 32,936), and three skills land at 32,672. I then property-tested it over 300 random cases — 1–6 skills, bodies from 1 B to 34 KB, trailer present or absent: worst file 32,766 bytes, zero over the cap, and the trailer present in every case that asked for one.

Four accounting mutations each redden an arm: reverting total to 0, counting the preamble but not the trailer, charging a section without its separator, and counting the trailer while not writing it. The last pair is the one I'd have worried about — budget and render drifting apart — and both directions are witnessed.

Moving ${text}\n out of the writer and into the budgeted trailer is the right shape: the quantity the cap applies to and the bytes that hit the disk are now the same string, so a future caller can't reintroduce the gap by adding its own suffix without declaring it.

Item 2 — the source claim holds at the tag, checked against the public repo. Both citations are accurate at rust-v0.162.1:

  • codex-home/src/instructions/mod.rs::load_from_codex_home walks [AGENTS.override.md, AGENTS.md] under $CODEX_HOME, tokio::fs::reads the whole file, from_utf8_lossy then .trim(), and returns it as Instructions. There is no byte cap on that path.
  • core/src/agents_md.rs::load_project_instructions builds loaded from user_instructions first, then sets remaining = config.project_doc_max_bytes and decrements it only for entries returned by read_agents_md per turn environment. So the global file is added outside the budget and never decrements it.

That is exactly what the new comment says, and it settles my question in the direction opposite to the one I guessed: the skills never share the 32 KiB with a workspace AGENTS.md, and nothing on this path truncates. Saying "the cap is our own ceiling" rather than "Codex truncates past it" is now a sourced statement.

Claude is untouched, verified by bytes rather than by reading. Rendering the same three skills through both heads' skillsAsText with no trailer gives byte-identical text (2,713 bytes, same sha256 prefix). The disclosed change is real and correctly scoped: Claude's 96 KiB budget now also counts the preamble and separators, so its admission boundary moves by 165 + 2n bytes. That tightens a self-imposed prompt budget with no external consumer, which is why it needs no arm of its own.

One residual, named rather than held, and it falls out of the source you just cited. load_from_codex_home tries AGENTS.override.md first and returns on the first non-empty file, so an override in the public home would shadow the skills entirely — while writePublicCodexInstructions removes only AGENTS.md when nothing is declared. It is not a defect today: the string AGENTS.override appears nowhere in this repo at this head, so nothing we ship can create one, and the public home is ours by construction. It is one rm away from being closed if you ever want the belt — worth a line in that comment at most, since the precedence is now documented three lines above it.

Suites. Full cli at this head: 1,145 passed, with the same six suites failing to run that fail at the base 7e21f486 on Cannot find module 'commander' from a borrowed node_modules — machine-local, controlled for, and +9 passing against that base. Product tree restored to 3e492c17 after every mutation.

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

GATE: PASS

@lilyshen0722
lilyshen0722 marked this pull request as ready for review October 21, 2026 20:30
@lilyshen0722
lilyshen0722 added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit 9aa0094 Oct 21, 2026
22 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