mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
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:
1 parent
df2bdc8a9e
commit
4eef417da6
8 files changed
+95
-54
No files matched your search
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user