v0.4.0

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.

md
---
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_eventFires when
session_startAn agent session begins or resumes.
session_endAn agent session terminates.

Prompt

hook_eventFires when
user_prompt_submitAfter 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_eventFires when
pre_tool_useBefore any tool call executes. Can block.
post_tool_useAfter any tool call succeeds.
post_tool_use_failureAfter 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_eventFires when
pre_shell_executionBefore a shell command tool call.
post_shell_executionAfter a shell command tool call.
pre_mcp_executionBefore an MCP tool call.
post_mcp_executionAfter an MCP tool call.
pre_read_fileBefore the agent reads a file.
post_file_editAfter the agent edits a file.

Permission

hook_eventFires when
permission_requestThe harness requests user permission for a sensitive action.
permission_deniedA tool call is denied (by the user, by policy, or by an auto-deny classifier).

Subagent

hook_eventFires when
subagent_startA subagent (delegated child) is spawned.
subagent_stopA subagent finishes.

Turn

hook_eventFires when
stopThe agent finishes responding (end of turn).

Compaction

hook_eventFires when
pre_compactBefore context compaction.
post_compactAfter context compaction completes.

Notifications

hook_eventFires when
notificationThe 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:

shell
hook_action: |
  INPUT=$(cat)
  echo "$INPUT" >> ~/.podium/sessions.log

For structured handling, use jq with defaults so the action stays portable across harness versions:

shell
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:

yaml
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. The sandbox_profile row of the §6.7.1 capability matrix is ✗ for codex, so a hook that sets the field fails materialization onto Codex with materialize.untranslatable even though Codex translates the hook_event itself.
  • 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

text
finance/audit/log-session-end/
├── ARTIFACT.md
└── scripts/
    └── log.sh

ARTIFACT.md:

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:

shell
#!/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.