Hooks

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: stop
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 (e.g., a formatter on file edits, a secrets scanner on shell commands). The adapter wires the subtype to the harness’s native subtype event when one exists, or installs a generic hook with a tool-name matcher otherwise.

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. When an artifact declares target_harnesses:, lint rejects ingest if a named harness does not support the chosen event. When target_harnesses: is absent, ingest stays permissive and the unsupported event is caught at materialization: a load_artifact onto a harness that cannot translate the event fails with materialize.untranslatable (§6.9).

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

For 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.csv

Declare 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_action runs on the host with the user’s privileges. Treat hooks like any other script the catalog ships: review, sign, and consider sandboxing. Set sandbox_profile: for sensitive hooks so a host with sandbox capability can constrain the action.
  • 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 alongside ARTIFACT.md and 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 jq defaults (jq -r '.field // empty') or guard against missing fields in shell.
  • Pick the canonical event closest to the intent. pre_tool_use covers 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: stop
hook_action: |
  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 (scripts/log.sh < payload.json), the logic is in one place, and the YAML stays readable.