Skip to content

fix(claude): make Edit/Write hooks read tool data from stdin, not env#471

Merged
turegjorup merged 1 commit into
release/3.0.0from
fix/claude-hooks-stdin-contract
May 29, 2026
Merged

fix(claude): make Edit/Write hooks read tool data from stdin, not env#471
turegjorup merged 1 commit into
release/3.0.0from
fix/claude-hooks-stdin-contract

Conversation

@turegjorup

Copy link
Copy Markdown
Contributor

Problem

The inline Edit|Write hooks in .claude/settings.json branched on $CLAUDE_FILE_PATH (and the addSql guard on $CLAUDE_NEW_STRING/$CLAUDE_CONTENT). Claude Code does not populate those env vars — it passes tool data as JSON on stdin (.tool_input.file_path, …) and exposes only $CLAUDE_PROJECT_DIR. As a result every guard and formatter silently no-op'd, and the PreToolUse guards used a non-blocking exit 1.

Verified empirically (not assumed)

Probed the live runtime on Claude Code 2.1.156 (jq 1.7.1):

Probe Result
CLAUDE_FILE_PATH in env? No
CLAUDE_PROJECT_DIR in env? Yes
stdin JSON has .tool_input.file_path? Yes
jq -r '.tool_input.file_path' extracts it? Yes

Then confirmed the old config was inert: a scratch migration with $this->addSql(...) wrote unblocked, and a formatting deviation was not corrected by php-cs-fixer.

Fix (hooks only)

  • Read the path via jq -r '.tool_input.file_path // empty'.
  • Derive REL_PATH from $CLAUDE_PROJECT_DIR for in-container tools.
  • PreToolUse guards block with exit 2 (message to stderr) instead of the non-blocking exit 1.
  • addSql guard payload spans new_string / content / edits[].new_string → catches Edit, Write, and MultiEdit.
  • Widen every Edit|Write matcher to Edit|MultiEdit|Write.
  • Add phpstan-baseline.neon to the generated/locked deny glob (closes drift with CLAUDE.md).
  • PostToolUse formatters stay fail-open (|| true).

Diff touches only the hooks object — permissions, plugins, and MCP config are byte-identical.

SessionStart prerequisite notice

Added a SessionStart (matcher startup|resume|clear) check that warns when jq or intelephense is missing on the host — writes a single notice to stderr and exit 2 (shown to the user, non-blocking). Without jq the corrected hooks fail open and silently no-op again, so the notice makes that setup requirement visible. The notice is platform-neutral; install steps live in CLAUDE.md.

Verification of the fix

  • addSql guard → exit 2 for Write/Edit/MultiEdit payloads; passes clean migrations and non-migration files.
  • Generated-path guard → exit 2 for composer.lock, phpstan-baseline.neon, generated-api.ts; passes ordinary source.
  • php-cs-fixer hook corrected $x=1 ;$x = 1; (confirms REL_PATH + container workdir).
  • Live in-session: a real Write of a migration containing $this->addSql was rejected with:

    BLOCKED: $this->addSql() is disallowed in migrations/ (ADR 010 — use Doctrine Schema tool API: $schema->createTable(...), $table->addColumn(Types::*, ...), etc.)

  • Negative control (clean src/**.php) passed.

⚠️ Host prerequisite

jq must be installed where Claude Code runs. Without it the corrected hooks fail open and silently no-op — which is exactly what the new SessionStart notice surfaces. CLAUDE.md now documents this.

🤖 Generated with Claude Code

The inline Edit|Write hooks branched on $CLAUDE_FILE_PATH /
$CLAUDE_NEW_STRING / $CLAUDE_CONTENT, which Claude Code does not
populate — it passes tool data as JSON on stdin and exposes only
$CLAUDE_PROJECT_DIR. Every guard and formatter therefore silently
no-op'd, and the PreToolUse guards used a non-blocking `exit 1`.

Verified empirically on Claude Code 2.1.156: the runtime injects
.tool_input.file_path on stdin and no $CLAUDE_FILE_PATH in the env.

Changes (hooks only):
- Read the path via `jq -r '.tool_input.file_path // empty'`.
- Derive REL_PATH from $CLAUDE_PROJECT_DIR for in-container tools.
- PreToolUse guards now block with `exit 2` (message to stderr).
- addSql guard spans new_string/content/edits[].new_string so it
  catches Edit, Write and MultiEdit.
- Widen every Edit|Write matcher to Edit|MultiEdit|Write.
- Add phpstan-baseline.neon to the generated/locked deny glob.
- PostToolUse formatters stay fail-open (`|| true`).

Also add a SessionStart notice that warns (stderr, non-blocking
exit 2) when `jq` or `intelephense` is missing on the host, since
without jq the hooks fail open and silently no-op again. Install
steps stay in CLAUDE.md (platform-neutral notice).

CLAUDE.md: document jq as a host prerequisite and note the hooks
fail open without it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@turegjorup turegjorup self-assigned this May 29, 2026
@turegjorup
turegjorup merged commit 13ac191 into release/3.0.0 May 29, 2026
19 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant