mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-18 19:02:18 +02:00
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:
co-authored by
Claude Fable 5
parent
4fe1e17038
commit
4c4584db12
+18
-17
@@ -32,7 +32,7 @@ The generated external invocation name is `gstack-claude`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Step 0: Check Claude CLI
|
## Step 0: Resolve Claude CLI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CLAUDE_BIN=$(command -v claude 2>/dev/null || echo "")
|
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:
|
If `NOT_FOUND`, stop and tell the user:
|
||||||
"Claude CLI not found. Install Claude Code, then re-run this skill."
|
"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
|
Resolve the binary and invoke it in the same host execution context. Do not
|
||||||
if [ -f "$HOME/.claude/.credentials.json" ] || [ -n "${ANTHROPIC_API_KEY:-}" ]; then
|
resolve it inside a sandbox and then run a different `claude` from another PATH.
|
||||||
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."
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -178,7 +175,8 @@ cat "$DIFF_FILE" >> "$PROMPT_FILE"
|
|||||||
3. Run Claude:
|
3. Run Claude:
|
||||||
|
|
||||||
```bash
|
```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:
|
4. Present the parsed output:
|
||||||
@@ -224,7 +222,8 @@ cat "$DIFF_FILE" >> "$PROMPT_FILE"
|
|||||||
3. Run Claude:
|
3. Run Claude:
|
||||||
|
|
||||||
```bash
|
```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:
|
4. Present the parsed output:
|
||||||
@@ -276,13 +275,15 @@ EOF
|
|||||||
For a new session:
|
For a new session:
|
||||||
|
|
||||||
```bash
|
```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:
|
For a resumed session:
|
||||||
|
|
||||||
```bash
|
```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:
|
4. Parse and save the session id:
|
||||||
@@ -324,7 +325,7 @@ rm -f "$PROMPT_FILE" "$RESP_FILE" "$ERR_FILE"
|
|||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
- **Binary not found:** Stop with install instructions.
|
- **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.
|
- **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`.
|
- **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."
|
- **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 fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
|
|
||||||
const ROOT = path.resolve(import.meta.dir, '..');
|
const ROOT = path.resolve(import.meta.dir, '..');
|
||||||
const MAX_SKILL_DESCRIPTION_LENGTH = 1024;
|
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');
|
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-claude', 'SKILL.md'), 'utf-8');
|
||||||
expect(content).toContain('claude -p');
|
expect(content).toContain('claude -p');
|
||||||
expect(content).toContain('mktemp /tmp/gstack-claude-prompt-');
|
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).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).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('--disable-slash-commands');
|
||||||
expect(content).toContain('--tools ""');
|
expect(content).toContain('--tools ""');
|
||||||
expect(content).toContain('--allowedTools Read,Grep,Glob');
|
expect(content).toContain('--allowedTools Read,Grep,Glob');
|
||||||
expect(content).toContain('--disallowedTools Bash,Edit,Write');
|
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');
|
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', () => {
|
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');
|
const shipContent = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8');
|
||||||
expect(shipContent).not.toContain('codex review --base');
|
expect(shipContent).not.toContain('codex review --base');
|
||||||
|
|||||||
Reference in New Issue
Block a user