fix(skill): keep the §0 bootstrap intact when Claude Code loads the skill with arguments

Claude Code rewrites a skill body that it loads with arguments: every bare
$1, $2 ... becomes the matching word of the request (0-based). Nine lines of
the agent preamble used bare positional parameters, so an agent that pasted
the §0 bootstrap (every session without a pre-seeded preamble: Agent Skill
injection off, the default, or a remote or Docker session) wrote helpers
that queried session "5" with timeout=claude and looked up "5=" instead of
CODEMAN_PASSWORD= in the .env, sending every call unauthenticated.

- $1/$2 -> ${1}/${2} in preamble.sh and the identical §0 heredoc in
  SKILL.md; same bash, and Claude Code's pattern (\$\d+(?!\w)) skips it.
- CODEMAN_PREAMBLE 1.33.4 -> 1.41.2, so a cache file written by the broken
  bootstrap (which carries the old stamp) is rewritten on the next run.
- A comment at the top of the preamble states the rule.
- New test: Claude Code 2.1.300's substitution applied to SKILL.md with
  sample arguments must leave the body, and the pasted heredoc, unchanged.
  It fails on the old file with the exact sessions/5 and timeout=claude lines.
- plugins/codeman re-synced; patch changeset.

Verified with real Claude Code 2.1.300 (the delivered heredoc is now
byte-identical to preamble.sh; before, 9 lines differed) and by running the
pasted helpers against a local recording server.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-15 19:33:09 +02:00
1 parent df2bdc8a9e
commit 4eef417da6
8 files changed
+95 -54

No files matched your search

+24
View File
@@ -163,6 +163,30 @@ describe('preamble single-source (seed + §0 heredoc parity)', () => {
expect(preamble).toBe(heredoc);
});
it("SKILL.md's shell survives Claude Code's skill-argument substitution unchanged", async () => {
// When Claude Code loads a skill WITH arguments (the Skill tool passes the user's request words),
// it rewrites the skill body before the model sees it. Measured on 2.1.300: every `$ARGUMENTS[N]`,
// `$ARGUMENTS` and bare `$N` (digits, then no word character) becomes the matching argument, 0-based,
// and a backslash before `$` is consumed as an escape. A bare `$1` in §0 arrived as
// `sessions/5/terminal` and `timeout=claude` for "start 5 claude workers", so an agent pasting §0
// wrote a broken preamble. `${1}` is the same bash and is never touched; keep it that way.
const skillMd = await readFile(join(packagedDir, 'SKILL.md'), 'utf-8');
const substitute = (body: string, args: string[]): string =>
body
.replace(/(?<!\\)\\\$(?=\d|ARGUMENTS)/g, '$')
.replace(/\$ARGUMENTS\[(\d+)\]/g, (m, i: string) => args[Number(i)] ?? m)
.replace(/\$(\d+)(?!\w)/g, (m, i: string) => args[Number(i)] ?? m)
.replaceAll('$ARGUMENTS', args.join(' '));
const args = 'start 5 claude workers, one per TODO item'.split(/\s+/);
expect(substitute(skillMd, args)).toBe(skillMd);
// And the heredoc an agent pastes from the substituted body is still the seeded preamble.
const rendered = substitute(skillMd, args);
const start = rendered.indexOf("<<'PREAMBLE'\n") + "<<'PREAMBLE'\n".length;
const preamble = await readFile(join(packagedDir, 'preamble.sh'), 'utf-8');
expect(rendered.slice(start, rendered.indexOf('\nPREAMBLE\n', start) + 1)).toBe(preamble);
});
it('seedAgentSessionPreamble writes the stamped preamble to the XDG cache path, 0600', async () => {
const prevXdg = process.env.XDG_CACHE_HOME;
const cacheDir = join(casePath, 'xdg-cache');