anthropics / anthropics/claude-code

[BUG] PreToolUse hook without the executable bit fails open: deny guards silently stop guarding, one Permission denied warning per tool call

オープン
#94,362 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る
area:hooks area:security bug has repro platform:macos
主要言語
Python
スター
145k
フォーク
23.1k
PR マージ指標
PR 指標を取得中

説明

## Environment

- Claude Code 2.1.270
- macOS 26.6.2 (Darwin 25.6.0), arm64, zsh
- Hooks in user-level `~/.claude/settings.json`, absolute command paths

## Summary

A `PreToolUse` hook whose command file is not executable cannot be spawned. Claude Code reports
that as a **non-blocking** failure, so the tool call proceeds — including when the hook's entire
job is to return `permissionDecision: "deny"`. The guard is off, nothing blocks, and the only
signal is a two-line warning printed **once per tool call, forever**:

```
PreToolUse:Bash hook error
Failed with non-blocking status code: /bin/sh: /path/to/guard.py: Permission denied
```

The message names `/bin/sh` and `Permission denied` but never the cause (`chmod +x`), so it reads
like a sandbox or TCC problem rather than a file mode.

## Reproduction

`guard.py` — a deny-everything PreToolUse hook, deliberately not executable:

```bash
mkdir -p /tmp/hookrepro && cd /tmp/hookrepro
cat > guard.py <<'EOF'
#!/usr/bin/env python3
import json
print(json.dumps({"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "DENIED BY GUARD"}}))
EOF
chmod 644 guard.py # <-- the whole bug
cat > settings.json <<'EOF'
{"hooks": {"PreToolUse": [{"matcher": "Bash",
"hooks": [{"type": "command", "command": "/tmp/hookrepro/guard.py"}]}]}}
EOF

claude -p "Run exactly one bash command: echo REPRO_MARKER. Then stop." \
--settings /tmp/hookrepro/settings.json --allowed-tools Bash \
--output-format stream-json --verbose < /dev/null
```

### Observed — `chmod 644` (not executable)

The command runs. The deny never happens:

```json
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"...","type":"tool_result",
"content":"REPRO_MARKER","is_error":false}]}}
```

### Expected, and what `chmod 755 guard.py` actually gives

```json
{"type":"user","message":{"role":"user","content":[{"type":"tool_result",
"content":"DENIED BY GUARD","is_error":true,"tool_use_id":"..."}]}}
```

Same session, same settings, same prompt — the only difference is the file mode.

## Two further gaps this exposes

1. **Nothing in `stream-json`.** In both runs the transcript carries `hook_started` /
`hook_response` events for `SessionStart` and **none at all for `PreToolUse`**. In headless /
SDK use the failure is completely invisible: no event, no warning, no non-zero exit. The
interactive TUI's warning is the only place it surfaces.
2. **No deduplication.** The warning is emitted per tool call. In the session where I hit this,
34 Bash calls produced 34 identical two-line blocks. Scrolled back it reads like a runaway
loop, which is how I noticed at all.

## Why this is worth blocking on

Losing the executable bit is ordinary and silent: a file committed as `100644` and checked out on
a new machine, `cp` from a template, an editor that rewrites rather than edits in place, an
unzipped release, a `rsync` without `-p`. In my case the hook was committed `100644` once and the
guard it implements — a check that blocks a specific class of outward-facing text — was off for an
entire session while dutifully printing a warning nobody reads as "your guard is disabled".

A `PreToolUse` hook that can return `deny` is a safety control. A safety control that cannot be
executed should not fail open.

## Suggested handling

- Treat a spawn failure of a `PreToolUse` hook (EACCES / exit 126 / ENOENT) as **blocking** by
default, or require an explicit opt-in such as `"failOpen": true` per hook to keep today's
behaviour.
- Name the actual cause in the message: `hook command is not executable — chmod +x `.
- Warn once per session per hook, not once per tool call.
- Report unrunnable hooks in `/doctor` and mark them in `/hooks`.
- Emit `hook_started` / `hook_response` for `PreToolUse` in `--output-format stream-json`.

## Related

Same fail-open family, different trigger (deleted session cwd → `posix_spawn` ENOENT):
#67147, #65378, #76808. Also #88578 (Windows backslash paths → hook never executes, silently).
None of them covers a hook file that exists and is readable but lacks `+x`.

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。