garrytan / garrytan/gstack

Multiple Claude Code security hook triggers in `/codex` and `/autoplan` skill templates

Open
#1,329 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
133k
Forks
19.9k
Avg merge
18h 46m
Merged PRs (30d)
26

Description

## Problem

Running `/codex` or `/autoplan` inside a Claude Code agent session triggers multiple built-in PreToolUse security hooks across different steps of the skill flow, causing repeated user confirmation prompts that break automated execution.

## Affected patterns

### Pattern 1 — `source gstack-codex-probe` (Step 0 and 0.5)

```bash
source ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null
```

**Hook message**: `'source' evaluates arguments as shell code`

Claude Code flags all `source` builtins unconditionally. `gstack-codex-probe`'s own header documents the intended usage ("Sourced from template bash blocks; never execute directly"), but the hook has no way to know the file is a zero-side-effect function library.

### Pattern 2 — `eval "$(gstack-paths)"` (Step 0.6)

```bash
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"
```

**Hook message**: `Unhandled node type: string`

Claude Code's static bash AST parser cannot handle tilde-path inside `$()` inside a double-quoted `eval` argument — the resulting `string` node is abandoned. `gstack-paths` explicitly documents `eval "$(gstack-paths)"` as its intended calling convention in the file header.

### Pattern 3 — `cd "$_REPO_ROOT"` after `git rev-parse` (Review / Challenge / Consult modes)

```bash
_REPO_ROOT=$(git rev-parse --show-toplevel) || { echo "ERROR: not in a git repo" >&2; exit 1; }
cd "$_REPO_ROOT"
_gstack_codex_timeout_wrapper 330 codex review ...
```

**Hook message**: C-class (`changes directory before running git`) or no message (silent CWD pollution). `codex exec` already accepts `-C ` to set the working directory; the `cd` is unnecessary for those modes.

### Pattern 4 — inline `python3 -u -c "..."` multi-line blocks (Challenge / Consult modes)

```bash
... | PYTHONUNBUFFERED=1 python3 -u -c "
import sys, json
...
# Fix 2: completeness check — warn if no turn.completed received
if turn_completed_count == 0:
...
"
```

**Two hooks fire simultaneously**:
- AP1 complexity (multi-line + embedded language + nested quotes = score 3/5)
- **Hook message**: `Newline followed by # inside a quoted argument can hide arguments from path validation` — the `# Fix N:` inline Python comments trigger B-class injection detection

This is the highest-impact pattern because it sits in the main execution paths for Challenge and Consult modes.

## Impact

A single `/codex review` run can require **4 manual confirmations** before Codex actually starts. Challenge and Consult modes are similarly affected. This defeats automated execution.

## Suggested fixes

### Fix 1 — Convert `gstack-codex-probe` functions to standalone binaries (eliminates Pattern 1)

Each `_gstack_codex_*` function becomes a direct binary call, removing the need for `source` entirely:

```bash
# Before
source ~/.claude/skills/gstack/bin/gstack-codex-probe 2>/dev/null
if ! _gstack_codex_auth_probe >/dev/null; then
_gstack_codex_log_event "codex_auth_failed"
_gstack_codex_timeout_wrapper 330 codex review ...

# After
if ! ~/.claude/skills/gstack/bin/gstack-codex-auth-probe >/dev/null; then
~/.claude/skills/gstack/bin/gstack-codex-log-event "codex_auth_failed"
~/.claude/skills/gstack/bin/gstack-codex-timeout 330 codex review ...
```

Note: `gstack-codex-log-event` currently reads `$_TEL` set by the caller. As a standalone binary it should call `gstack-config get telemetry` internally or accept a `--telemetry on/off` flag.

### Fix 2 — Replace `eval "$(gstack-paths)"` with individual `--get` calls (eliminates Pattern 2)

```bash
# Before
eval "$(~/.claude/skills/gstack/bin/gstack-paths)"

# After — 3 separate bash calls, no eval
GSTACK_STATE_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get state-root)
PLAN_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get plan-root)
TMP_ROOT=$(~/.claude/skills/gstack/bin/gstack-paths --get tmp-root)
```

Requires adding `--get ` support to `gstack-paths`; existing bare invocation should remain backward-compatible.

### Fix 3 — Replace `cd "$_REPO_ROOT"` with subshell or `-C` flag (eliminates Pattern 3)

`codex exec` already accepts `-C `. For `codex review`, use a subshell to avoid CWD pollution:

```bash
# Before
cd "$_REPO_ROOT"
codex review ...

# After (subshell — does not pollute session CWD)
( cd "$_REPO_ROOT" && codex review ... )
```

### Fix 4 — Extract inline Python JSONL parser to a standalone script (eliminates Pattern 4, highest priority)

```bash
# Before — inline multi-line Python with # comments triggers two hooks
... | python3 -u -c "
import sys, json
# Fix 2: completeness check
...
"

# After — extract to bin/gstack-codex-jsonl-parser
... | ~/.claude/skills/gstack/bin/gstack-codex-jsonl-parser
```

This is the highest-priority fix: it removes both the AP1 complexity violation and the B-class `# comment` injection detection in one change, and affects the most-used flows (Challenge and Consult).

## Environment

- Claude Code (latest, 2026-05-05)
- gstack global install: `~/.claude/skills/gstack/`
- macOS

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.