Hooks
Lifecycle observers with a canonical hook_event taxonomy and a shell hook_action.
A hook artifact wires a shell action into a harness lifecycle event. Use it to log, notify, run a check, inject context, or otherwise observe and influence the agent loop.
---
type: hook
version: 1.0.0
hook_event: session_end
hook_action: |
INPUT=$(cat)
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] session end: $INPUT" \
>> ~/.podium/session-audit.log
---hook_event is one of the canonical event names defined by Podium (see Canonical events below). The harness adapter translates the canonical name into the harness's native event vocabulary at materialization time.
hook_action is a shell snippet executed when the event fires. The harness writes a JSON payload to the action's stdin.
Canonical events
The canonical event taxonomy stays harness-agnostic. The adapter does the translation. Events are grouped by concern.
Session lifecycle
hook_event | Fires when |
|---|---|
session_start | An agent session begins or resumes. |
session_end | An agent session terminates. |
Prompt
hook_event | Fires when |
|---|---|
user_prompt_submit | After the user submits a prompt, before the model processes it. Can inject context or block. |
Tool calls (generic)
These cover every tool call regardless of the underlying tool.
hook_event | Fires when |
|---|---|
pre_tool_use | Before any tool call executes. Can block. |
post_tool_use | After any tool call succeeds. |
post_tool_use_failure | After a tool call fails (error, timeout, denied). |
Tool calls (subtypes)
Subtype events target a specific kind of tool call. Use them when the action only applies to that category, such as a formatter on file edits or a secrets scanner on shell commands. When a harness exposes a native subtype event, the adapter wires the subtype straight onto it: Cursor maps pre_shell_execution, pre_mcp_execution, pre_read_file, and post_file_edit onto beforeShellExecution, beforeMCPExecution, beforeReadFile, and afterFileEdit. When a harness exposes only the generic tool events, the adapter maps the subtype onto the generic native event and installs a tool-name matcher, so only tool calls in that category fire the action. Claude Code receives a shell subtype as a PreToolUse or PostToolUse entry matching ^Bash$, an MCP subtype matching ^mcp__, pre_read_file matching ^Read$, and post_file_edit matching ^(Edit|Write|NotebookEdit)$. Codex matches its shell subtypes on ^Bash$, and Gemini matches them on ^run_shell_command$.
The matcher narrows by tool name alone. An action that depends on the arguments of the call still reads them from the payload on stdin.
hook_event | Fires when |
|---|---|
pre_shell_execution | Before a shell command tool call. |
post_shell_execution | After a shell command tool call. |
pre_mcp_execution | Before an MCP tool call. |
post_mcp_execution | After an MCP tool call. |
pre_read_file | Before the agent reads a file. |
post_file_edit | After the agent edits a file. |
Permission
hook_event | Fires when |
|---|---|
permission_request | The harness requests user permission for a sensitive action. |
permission_denied | A tool call is denied (by the user, by policy, or by an auto-deny classifier). |
Subagent
hook_event | Fires when |
|---|---|
subagent_start | A subagent (delegated child) is spawned. |
subagent_stop | A subagent finishes. |
Turn
hook_event | Fires when |
|---|---|
stop | The agent finishes responding (end of turn). |
Compaction
hook_event | Fires when |
|---|---|
pre_compact | Before context compaction. |
post_compact | After context compaction completes. |
Notifications
hook_event | Fires when |
|---|---|
notification | The harness sends a system notification (waiting for input, idle prompt, and similar). |
Coverage varies by harness
Not every harness implements every event in the canonical list. Coverage is graded per harness rather than per event. When an artifact declares target_harnesses:, ingest lint errors for a named harness whose hook_event grade is ✗ (claude-desktop, claude-cowork, opencode, pi, and hermes) and warns for the ⚠ grade (cursor). When target_harnesses: is absent, ingest stays permissive and a ✗ harness is caught at materialization: a load_artifact onto it fails with materialize.untranslatable (§6.9). A harness graded ✓ or ⚠ that has no native mapping for the specific canonical event writes no hook and reports no error, so confirm the event against the harness's own hook documentation before relying on a less common one.
For the events a specific harness emits, refer to that harness's hook documentation. The harness's own docs are the source of truth, since each vendor's surface evolves independently. The full roster of supported harnesses (with adapter values and documentation links) is in Configure your harness.
Payload handling
The harness writes a JSON payload to stdin. The schema is harness-defined and event-defined. Common fields appear across most harnesses (session identifier, working directory, tool name and arguments for tool events, prompt text for user_prompt_submit), but the exact field set varies.
A simple action reads the payload as a string:
hook_action: |
INPUT=$(cat)
echo "$INPUT" >> ~/.podium/sessions.logFor structured handling, use jq with defaults so the action stays portable across harness versions:
hook_action: |
INPUT=$(cat)
CONV_ID=$(echo "$INPUT" | jq -r '.session_id // .conversation_id // "unknown"')
echo "$CONV_ID,$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
>> ~/.podium/session-stats.csvDeclare the dependency:
runtime_requirements:
system_packages: [jq]The harness refuses to materialize when a system package isn't available.
Authoring guidance
- Hooks ship code. A hook's
hook_actionruns on the host with the user's privileges. Treat hooks like any other script the catalog ships: review, sign, and consider sandboxing. Setsandbox_profile:for sensitive hooks so a host with sandbox capability can constrain the action. Thesandbox_profilerow of the §6.7.1 capability matrix is ✗ forcodex, so a hook that sets the field fails materialization onto Codex withmaterialize.untranslatableeven though Codex translates thehook_eventitself. - Keep actions short. A long shell action embedded in YAML gets ugly. Move complex logic into a bundled script (in
scripts/) and have the action invoke it. The script lives alongsideARTIFACT.mdand ships with the hook. - Make the description specific. "Log session-end events to a local audit file." is fine. "Lifecycle observer." is too vague to surface in search.
- Don't depend on payload fields. Harnesses change their payload schema over time. Use
jqdefaults (jq -r '.field // empty') or guard against missing fields in shell. - Pick the canonical event closest to the intent.
pre_tool_usecovers shell, MCP, file-edit, and any other tool call uniformly; the adapter translates to whichever native event the harness emits. Selecting a more specific harness-native event by working around the canonical taxonomy makes the artifact non-portable.
Example: bundled-script pattern
finance/audit/log-session-end/
├── ARTIFACT.md
└── scripts/
└── log.sh
ARTIFACT.md:
---
type: hook
name: log-session-end
version: 1.0.0
description: Log session-end events to a local audit file.
tags: [hook, audit]
sensitivity: low
hook_event: session_end
hook_action: |
bash scripts/log.sh
runtime_requirements:
system_packages: [jq]
---scripts/log.sh:
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
LOG_FILE="${HOME}/.podium/session-audit.log"
CONV_ID=$(echo "$INPUT" | jq -r '.session_id // .conversation_id // "unknown"')
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "[${TIMESTAMP}] session end: ${CONV_ID}" >> "${LOG_FILE}"The hook is now testable in isolation (bash scripts/log.sh < payload.json), the logic is in one place, and the YAML stays readable. Bundled resources materialize with mode 0644, so the action invokes the interpreter explicitly rather than executing the script path directly.