mirror of https://github.com/garrytan/gstack.git
fix(claude): stop false-blocking macOS keychain subscription auth in host detection
The /claude skill's auth probe only recognized env-var/API-key auth, so macOS subscription installs (keychain-backed, where `claude -p` works fine) were told they had no auth. Detection now uses host invocation. Fixes #1890. Contributed by @xing-qnex (PR #2411); PR #2548 by @shawnacalia covered the keychain case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4fe1e17038
commit
4c4584db12
|
|
@ -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 "<session-id>" --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 "<session-id>" --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."
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in New Issue