diff --git a/claude/SKILL.md.tmpl b/claude/SKILL.md.tmpl
index 94552cbe4..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.
---
@@ -95,8 +92,8 @@ Create temp files:
```bash
PROMPT_FILE=$(mktemp /tmp/gstack-claude-prompt-XXXXXX)
-RESP_FILE=$(mktemp /tmp/gstack-claude-response-XXXXXX.json)
-ERR_FILE=$(mktemp /tmp/gstack-claude-error-XXXXXX.txt)
+RESP_FILE=$(mktemp /tmp/gstack-claude-response-XXXXXX)
+ERR_FILE=$(mktemp /tmp/gstack-claude-error-XXXXXX)
```
Cleanup at the end of every mode:
@@ -151,7 +148,7 @@ Review the current branch diff with nested Claude in tool-less mode.
```bash
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
cd "$_REPO_ROOT"
-DIFF_FILE=$(mktemp /tmp/gstack-claude-diff-XXXXXX.patch)
+DIFF_FILE=$(mktemp /tmp/gstack-claude-diff-XXXXXX)
git fetch origin --quiet 2>/dev/null || true
git diff "origin/" > "$DIFF_FILE" 2>/dev/null || git diff "" > "$DIFF_FILE"
```
@@ -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 2fb783ffd..1de86fd86 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;
@@ -1777,16 +1778,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');