Hook Reference
The Claude Code hooks the Trenchcoat plugin registers, and how recorded events reach the API.
Claude Code hooks are scripts that run at defined points in the agent lifecycle. The plugin registers nine of them. They run out of process, receive their input as JSON on stdin, and cannot block Claude Code's main thread.
Registered hooks
Declared in the plugin's hooks/hooks.json:
| Hook | Script | Emits (wire name) | Captures |
|---|---|---|---|
SessionStart | session_start.py | session_start | Working directory; branch and eval IDs if enabled |
SessionEnd | session_end.py | session_end | Duration — and drains the push queue |
UserPromptSubmit | user_prompt_submit.py | prompt_submit | Prompt length and word count |
PreToolUse | pre_tool_use.py | tool_use or skill_use | Tool name, truncated input |
PostToolUse | post_tool_use.py | tool_result | Duration, result size, error state |
Stop | stop.py | assistant_stop | Stop reason, tokens, model |
SubagentStart | subagent_start.py | subagent_start | Agent ID, type, classification |
SubagentStop | subagent_stop.py | subagent_stop | Tool counts, turns, tokens, model |
PreCompact | pre_compact.py | pre_compact | Timestamp only |
SubagentStop has its own script — it is not folded into stop.py, which handles only the main assistant's turn.
PreToolUse
Receives the tool name and raw input as JSON on stdin. Truncates the input to privacy.tool_input_preview_chars characters (default 100) before recording, so a long input is never stored in full.
When the invoked tool is a Skill, this hook emits skill_use instead of tool_use and records an activation ID. Tool calls made while that skill is active carry the ID as their spawner_id, which is what attributes work to a skill.
PostToolUse
Receives the tool response and derives result_size, is_error, and error_preview from it. By default the result itself is not stored — only its size.
Duration is recorded with a duration_source of native, computed, or reconciled. A reconciled duration means the plugin never observed the tool finishing and closed the record at the end of the turn; treat it as an upper bound.
Stop
Fires when the main assistant finishes a turn. Also closes out any tool call still marked in-flight — those are the reconciled durations above — and clears the active skill context, so a skill is not credited for calls in a later, unrelated turn.
SubagentStop
Parses the subagent's transcript to derive tool counts, turn count, tokens, and model.
Where things live
Hook scripts ship with the plugin and are managed by Claude Code:
~/.claude/plugins/cache/pando-plugins/trenchcoat/<version>/hooks/They are plain Python and can be read directly. Editing them is not recommended — plugin updates replace the whole versioned directory.
Your data is somewhere else entirely, and is not touched by plugin updates:
~/.claude/trenchcoat/How events reach the API
Hook fires
│
├──► events-YYYY-MM-DD.jsonl always
│
└──► .push_queue.jsonl only if TRENCHCOAT_API_KEY is set
and the type is one the API accepts
│
▼
SessionEnd ──► flush in batches of push_batch_size (default 100)
POST /api/v1/events
│
├── all batches succeed ──► queue cleared
└── some fail ──────────► unsent events kept, retried next session endThree consequences worth knowing:
Uploads happen at session end, not on a timer. There is no flush interval and no size trigger. A long-running session's events are visible in the dashboard once it ends.
A failed upload loses nothing. Unsent events remain queued and go out with the next session's flush. /trenchcoat:doctor reports the queue depth.
Local recording is independent of upload. The JSONL file is written whether or not a key is configured, so /trenchcoat:report and /trenchcoat:dashboard work with no account at all.
Only event types the ingest schema accepts are queued. The API validates the entire batch, so a single unrecognized type would reject every event alongside it — a local-only type is written to JSONL and skipped for upload rather than risking that.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No events in the dashboard | Not connected, or the session has not ended yet | Run /trenchcoat:doctor. Uploads happen at session end — end the session and check again. |
doctor reports an invalid key | Key revoked, expired, or lacking read:sessions | Generate a new key in Settings → API Keys, update settings.json, start a new session, re-run /trenchcoat:connect. |
doctor shows OTel errors right after connecting | The running session still has the old environment | Start a new session. Claude Code loads env from settings.json at session start. |
| Push queue growing | API unreachable, or the key is rejected | Check connectivity and key validity. Events accumulate safely until a flush succeeds. |
| Sessions appear but tool events are missing | Hooks failing | Run a hook script directly against a sample JSON input and check stderr. |
| Prompts recorded but no text | Working as designed | privacy.log_prompt_content is false by default. |
| Dashboard branch filter always empty | Working as designed | privacy.capture_git_branch is false by default. |
| Content missing despite local flags set | The key's scopes do not permit it | Local OTEL_LOG_* flags do not decide what is stored — the key's scopes do, server-side. Widen the scopes and re-run /trenchcoat:connect. |
| Python error on hook fire | Wrong interpreter | python3 --version must be 3.10+ and on PATH. A TypeError mentioning ` |
Include the output of /trenchcoat:doctor in any bug report — it covers credentials, connectivity, event coverage, OTel configuration, and whether your local flags agree with your key's scopes.