anthropics / anthropics/claude-code

[BUG] stream-json emits hook_started/hook_response for SessionStart only; PreToolUse/PostToolUse/UserPromptSubmit/Stop hooks run with no event, so SDK consumers cannot see repo-supplied hooks execute

Open
#94,275 1 comment 0 reactions 0 assignees View on GitHub
area:hooks bug has repro
Dominant language
Python
Stars
145k
Forks
23.1k
PR merge metrics
PR metrics pending

Description

### Preflight Checklist

- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code

### What's Wrong?

With `--output-format stream-json --verbose`, `system/hook_started` and `system/hook_response`
events are emitted only for `SessionStart` hooks. Hooks on every other event execute normally
(side effects are observable) but produce no event in the stream, nothing on stderr, and the
`system/init` message has no field listing configured hooks.

Measured across 36 `claude -p` runs on 2.1.270 with hooks configured on PreToolUse, PostToolUse,
UserPromptSubmit, Stop, SessionEnd and SessionStart (both user-level and project-level):

```
$ cat results/*.jsonl | jq -r 'select(.type=="system" and (.subtype|startswith("hook"))) | .subtype+" "+.hook_event' | sort | uniq -c
36 hook_response SessionStart
36 hook_started SessionStart
```

Why this matters beyond debugging: project `.claude/settings.json` hooks are loaded and executed in
`-p` mode without any trust check (documented), and in `-p` mode there is no dialog. So an SDK
consumer that runs `claude -p` in a directory it did not author gets arbitrary command execution
on the first tool call, with no signal at any layer it can observe. In an interactive session the
`/cd` dialog at least warns "This directory configures hooks that run commands"; the SDK stream
gives nothing equivalent. `#27200` asked for the same thing purely as an observability feature;
this report adds the untrusted-directory angle because it changes the severity.

Confirmed with a marker hook: `PreToolUse` hook in a never-trusted repo's `.claude/settings.json`
fired (marker file written at 12:00:03Z) while the stream contained only the two SessionStart events.

### What Should Happen?

1. `hook_started` / `hook_response` should be emitted for every hook event type, not just
SessionStart, matching what #27200 requested.
2. `system/init` should list the hook sources that were loaded for the session (at minimum: which
settings file, which event, matcher, and command string), so a consumer can inspect what will run
before the first tool call.

### Error Messages/Logs

```shell
# system/init keys on 2.1.270 (no hook-related field):
["agents","analytics_disabled","apiKeySource","capabilities","claude_code_version","cwd",
"fast_mode_disabled_reason","fast_mode_state","mcp_servers","memory_paths",
"messaging_socket_path","model","output_style","permissionMode","plugins",
"product_feedback_disabled","session_id","skills","slash_commands","subtype",
"terminal_slash_commands","tools","type","uuid"]

# stderr of the run: empty
# marker log written by the PreToolUse hook:
STANDALONE_HOOK 2026-09-14T12:00:03Z
```

### Steps to Reproduce

1. Create a fresh git repo that has never been trusted:
```sh
mkdir -p /tmp/hookrepo/.claude && cd /tmp/hookrepo && git init -q
cat > .claude/settings.json <<'JSON'
{"hooks":{"PreToolUse":[{"matcher":"","hooks":[{"type":"command","command":"touch /tmp/hookrepo/HOOK_FIRED"}]}]}}
JSON
```
2. Run non-interactively with stream output:
```sh
claude -p 'Use the Bash tool to run exactly: ls -la . Then reply DONE.' \
--allowedTools 'Bash(ls:*)' --output-format stream-json --verbose > out.jsonl 2> err.txt
```
3. Observe:
```sh
ls /tmp/hookrepo/HOOK_FIRED # exists: hook ran
jq -r 'select(.type=="system") | .subtype+" "+(.hook_event//"")' out.jsonl
# init
# hook_started SessionStart (only if you have user-level SessionStart hooks)
# hook_response SessionStart
# -> no event for the PreToolUse hook
cat err.txt # empty
```
4. For comparison, add a SessionStart hook to the same file and rerun: it produces
`hook_started`/`hook_response` events.

### Claude Model

Other

### Is this a regression?

No, this never worked

### Last Working Version

n/a

### Claude Code Version

2.1.270

### Platform

Anthropic API

### Operating System

macOS

### Terminal/Shell

Non-interactive/CI environment

### Additional Information

- `--setting-sources user` and `--settings '{"disableAllHooks":true}'` both prevent the project
hook from running, so mitigations exist; the gap is that a consumer has no way to detect that
it needed one.
- Docs (Hooks reference, "Hook lifecycle" and SDK "stream-json" message types) don't mention that
only SessionStart emits events; if this is intended, a doc note would help, but the consistency
between event types suggests it isn't.
- Related closed reports: #27200 (duplicate-closed), #15066 (stale-closed).
- Full reproduction fixtures and per-run transcripts available on request.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reproducing the `claude -p` command with `--output-format stream-json --verbose` and the project `.claude/settings.json` hook configuration. Compare `system/init` and hook event output for SessionStart and PreToolUse, then verify that every configured hook event is observable before the first tool call and that the relevant sources are listed.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli, observability, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.