[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

Status Open
Reported on v2.1.270
Maintainer reply None cached
Activity 1 comment · opened Sep 14, 2026

Preflight Checklist

  • [x] I have searched existing issues 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.

  1. 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

# 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
``

  1. 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
``

  1. 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
``

  1. 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗