Plugin SDK & Hooks

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:

HookScriptEmits (wire name)Captures
SessionStartsession_start.pysession_startWorking directory; branch and eval IDs if enabled
SessionEndsession_end.pysession_endDuration — and drains the push queue
UserPromptSubmituser_prompt_submit.pyprompt_submitPrompt length and word count
PreToolUsepre_tool_use.pytool_use or skill_useTool name, truncated input
PostToolUsepost_tool_use.pytool_resultDuration, result size, error state
Stopstop.pyassistant_stopStop reason, tokens, model
SubagentStartsubagent_start.pysubagent_startAgent ID, type, classification
SubagentStopsubagent_stop.pysubagent_stopTool counts, turns, tokens, model
PreCompactpre_compact.pypre_compactTimestamp 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 end

Three 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

SymptomLikely causeFix
No events in the dashboardNot connected, or the session has not ended yetRun /trenchcoat:doctor. Uploads happen at session end — end the session and check again.
doctor reports an invalid keyKey revoked, expired, or lacking read:sessionsGenerate a new key in Settings → API Keys, update settings.json, start a new session, re-run /trenchcoat:connect.
doctor shows OTel errors right after connectingThe running session still has the old environmentStart a new session. Claude Code loads env from settings.json at session start.
Push queue growingAPI unreachable, or the key is rejectedCheck connectivity and key validity. Events accumulate safely until a flush succeeds.
Sessions appear but tool events are missingHooks failingRun a hook script directly against a sample JSON input and check stderr.
Prompts recorded but no textWorking as designedprivacy.log_prompt_content is false by default.
Dashboard branch filter always emptyWorking as designedprivacy.capture_git_branch is false by default.
Content missing despite local flags setThe key's scopes do not permit itLocal 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 fireWrong interpreterpython3 --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.

On this page