diff --git a/claude/SKILL.md.tmpl b/claude/SKILL.md.tmpl index e109f21f7..17483faec 100644 --- a/claude/SKILL.md.tmpl +++ b/claude/SKILL.md.tmpl @@ -32,7 +32,7 @@ The generated external invocation name is `gstack-claude`. --- -## Step 0: Check Claude CLI +## Step 0: Resolve Claude CLI ```bash CLAUDE_BIN=$(command -v claude 2>/dev/null || echo "") @@ -42,18 +42,15 @@ CLAUDE_BIN=$(command -v claude 2>/dev/null || echo "") If `NOT_FOUND`, stop and tell the user: "Claude CLI not found. Install Claude Code, then re-run this skill." -Check auth: +Do not infer authentication state from credential files or environment variables. +Claude Code may use an OS keychain that is unavailable inside the host agent's +sandbox. On hosts that sandbox shell execution, run the actual `claude -p` +invocation outside that sandbox using the host's normal approval mechanism. Only +report an authentication blocker when that actual invocation returns an auth, +login, or unauthorized error. -```bash -if [ -f "$HOME/.claude/.credentials.json" ] || [ -n "${ANTHROPIC_API_KEY:-}" ]; then - echo "AUTH_FOUND" -else - echo "AUTH_MISSING" -fi -``` - -If `AUTH_MISSING`, stop and tell the user: -"No Claude authentication found. Run `claude` interactively to log in, or export `ANTHROPIC_API_KEY`, then re-run this skill." +Resolve the binary and invoke it in the same host execution context. Do not +resolve it inside a sandbox and then run a different `claude` from another PATH. --- @@ -178,7 +175,8 @@ cat "$DIFF_FILE" >> "$PROMPT_FILE" 3. Run Claude: ```bash -cat "$PROMPT_FILE" | claude -p --output-format json --disable-slash-commands --tools "" > "$RESP_FILE" 2>"$ERR_FILE" +CLAUDE_BIN=$(command -v claude 2>/dev/null) || { echo "Claude CLI not found" >&2; exit 1; } +cat "$PROMPT_FILE" | "$CLAUDE_BIN" -p --output-format json --disable-slash-commands --tools "" > "$RESP_FILE" 2>"$ERR_FILE" ``` 4. Present the parsed output: @@ -224,7 +222,8 @@ cat "$DIFF_FILE" >> "$PROMPT_FILE" 3. Run Claude: ```bash -cat "$PROMPT_FILE" | claude -p --output-format json --disable-slash-commands --tools "" > "$RESP_FILE" 2>"$ERR_FILE" +CLAUDE_BIN=$(command -v claude 2>/dev/null) || { echo "Claude CLI not found" >&2; exit 1; } +cat "$PROMPT_FILE" | "$CLAUDE_BIN" -p --output-format json --disable-slash-commands --tools "" > "$RESP_FILE" 2>"$ERR_FILE" ``` 4. Present the parsed output: @@ -276,13 +275,15 @@ EOF For a new session: ```bash -cat "$PROMPT_FILE" | claude -p --output-format json --disable-slash-commands --allowedTools Read,Grep,Glob --disallowedTools Bash,Edit,Write > "$RESP_FILE" 2>"$ERR_FILE" +CLAUDE_BIN=$(command -v claude 2>/dev/null) || { echo "Claude CLI not found" >&2; exit 1; } +cat "$PROMPT_FILE" | "$CLAUDE_BIN" -p --output-format json --disable-slash-commands --allowedTools Read,Grep,Glob --disallowedTools Bash,Edit,Write > "$RESP_FILE" 2>"$ERR_FILE" ``` For a resumed session: ```bash -cat "$PROMPT_FILE" | claude -p --resume "" --output-format json --disable-slash-commands --allowedTools Read,Grep,Glob --disallowedTools Bash,Edit,Write > "$RESP_FILE" 2>"$ERR_FILE" +CLAUDE_BIN=$(command -v claude 2>/dev/null) || { echo "Claude CLI not found" >&2; exit 1; } +cat "$PROMPT_FILE" | "$CLAUDE_BIN" -p --resume "" --output-format json --disable-slash-commands --allowedTools Read,Grep,Glob --disallowedTools Bash,Edit,Write > "$RESP_FILE" 2>"$ERR_FILE" ``` 4. Parse and save the session id: @@ -324,7 +325,7 @@ rm -f "$PROMPT_FILE" "$RESP_FILE" "$ERR_FILE" ## Error Handling - **Binary not found:** Stop with install instructions. -- **Auth missing:** Stop with login/API key instructions. +- **Auth failure from the actual host invocation:** Stop with login/API key instructions. - **Auth failure from stderr:** Surface the stderr line and ask the user to re-authenticate. - **JSON parse failure:** Show raw stdout from `$RESP_FILE` and stderr from `$ERR_FILE`. - **Empty response:** Tell the user "Claude returned no response. Check stderr for errors." diff --git a/test/gen-skill-docs.test.ts b/test/gen-skill-docs.test.ts index a8ff74c29..36cb3d1d4 100644 --- a/test/gen-skill-docs.test.ts +++ b/test/gen-skill-docs.test.ts @@ -4,6 +4,7 @@ import { SNAPSHOT_FLAGS } from '../browse/src/snapshot'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; +import { spawnSync } from 'child_process'; const ROOT = path.resolve(import.meta.dir, '..'); const MAX_SKILL_DESCRIPTION_LENGTH = 1024; @@ -1802,16 +1803,39 @@ describe('Codex generation (--host codex)', () => { const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-claude', 'SKILL.md'), 'utf-8'); expect(content).toContain('claude -p'); expect(content).toContain('mktemp /tmp/gstack-claude-prompt-'); + expect(content).toContain('mktemp /tmp/gstack-claude-response-XXXXXX'); + expect(content).toContain('mktemp /tmp/gstack-claude-error-XXXXXX'); expect(content).toContain('mktemp /tmp/gstack-claude-diff-'); + expect(content).not.toMatch(/gstack-claude-(?:prompt|response|error|diff)-X{6,}\.\w+/); expect(content).not.toContain('/tmp/gstack-claude-diff-$$'); - expect(content).toContain('cat "$PROMPT_FILE" | claude -p'); + expect(content).toContain('cat "$PROMPT_FILE" | "$CLAUDE_BIN" -p'); + expect(content).toContain('Resolve the binary and invoke it in the same host execution context'); expect(content).toContain('--disable-slash-commands'); expect(content).toContain('--tools ""'); expect(content).toContain('--allowedTools Read,Grep,Glob'); expect(content).toContain('--disallowedTools Bash,Edit,Write'); + expect(content).toContain('Do not infer authentication state from credential files'); + expect(content).toContain('run the actual `claude -p`'); + expect(content).not.toContain('AUTH_MISSING'); + expect(content).not.toContain('$HOME/.claude/.credentials.json'); expect(content).toContain('is_error'); }); + test('Claude temp file templates are accepted by host mktemp', () => { + for (const template of [ + '/tmp/gstack-claude-prompt-XXXXXX', + '/tmp/gstack-claude-response-XXXXXX', + '/tmp/gstack-claude-error-XXXXXX', + '/tmp/gstack-claude-diff-XXXXXX', + ]) { + const result = spawnSync('mktemp', [template], { encoding: 'utf-8' }); + expect(result.status).toBe(0); + const created = result.stdout.trim(); + expect(created.startsWith(template.replace('XXXXXX', ''))).toBe(true); + fs.unlinkSync(created); + } + }); + test('Codex review step stripped from Codex-host ship and review', () => { const shipContent = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8'); expect(shipContent).not.toContain('codex review --base');