This documentation describes Poolside Agent CLI v1.0.16. Check your version with
pool --version. To update, exit any active session and run pool update from your terminal. See Poolside Agent CLI releases on GitHub for release history.What you can do with hooks
- Add custom checks that block matching commands, such as
sudoor force pushes. - Redact secrets or internal hostnames from tool output before the model reads it.
- Screen prompts against custom policies, or add standing context to every prompt.
- Announce workspace conventions at session start.
- Keep the agent working until it completes a required outcome, such as creating a summary file.
Configure your first hook
This example adds a hook that blocks a harmless test command, so you can verify the hook without risking a privileged action if the hook does not run. Prerequisites- You use a POSIX shell on macOS or Linux.
- You have installed
jq.
pool runs Windows hooks.
Steps
-
Create
<project-path>/.poolside/hooks/block-hook-test.shwith this content:.poolside/hooks/block-hook-test.sh -
Make the script executable:
-
Register the hook in
<project-path>/.poolside/settings.yaml. Create the file if it does not exist:.poolside/settings.yaml - Start a new agent session in the project.
-
Ask the agent to run
echo hook-test.
pool ignores decision fields it does not recognize, so a misspelled field can produce no error and have no effect.
How a hook runs
Every hook follows the same cycle:- The event fires.
poolreaches a lifecycle point, such as a tool that is about to run, and selects the hooks configured for that event. On tool events, only hooks whosematchermatches the tool name run. poolwrites the event JSON to the hook’sstdin. One JSON object describes the event: which event it is, the session it belongs to, and the event’s data, such as the tool name and its arguments. Read it once, at the start of your script.- Your script does its work and decides what should happen.
- Your script answers with an exit code, and optionally JSON on
stdout. Exit0with emptystdoutobserves and changes nothing. Exit0with a JSON object asks for a specific decision, such as denying a call or rewriting its arguments. Exit2blocks the event, with the text your script wrote tostderras the reason. See Exit codes for what blocking means at each event. poolapplies the decision and continues, telling the model what changed. If several hooks are configured for the event, they run one after another, and each one sees the previous hook’s rewrite.
Hook events
Notes on specific events:
PreToolUseruns before the approval prompt. A blocked call is never offered for approval, and a rewritten call is approved on its rewritten arguments rather than the ones the model proposed. A block does not end the turn: the model reads the block reason as the tool result and can try something else.- A
UserPromptSubmitblock returns an error to you, and the model never sees the prompt. Stophooks do not run when you cancel a turn yourself, so a hook can never override your stop.- A
Stopcontinuation arrives as a new input in the session, as if you had sent it yourself. It does not pass throughUserPromptSubmithooks, so hooks cannot feed into each other. Continuations do not reset or extend the session’s step budget; they keep drawing on the samemax_steps.
Write additional hook scripts
- Capture
stdinfirst withinput=$(cat). You can readstdina single time. Watch out for shell here-documents: withpython3 <<'EOF', the here-document becomes the child process’sstdinand hides the event JSON. Usepython3 -c '<code>'instead. - Write block reasons to
stderr, notstdout. - Print to
stdoutonly when you mean it. Any non-emptystdouton exit 0 is parsed as a decision, and output that does not parse marks the hook as failed. - Keep scripts fast. Set a tight
timeoutwhen the default 60 seconds is more than you need.
Protocol reference
All field names usesnake_case, in both directions. hook_api_version is currently 1.0. Changes within a major version are additive only.
Exit codes
Input payload
pool writes one JSON object to the hook’s stdin:
Example: PreToolUse payload
hook_api_version, hook_event_name, and event_id, which is opaque and shared by all hooks handling the same event occurrence, plus session_id, cwd, and trajectory_path when they are known. Your hook can use these to find session state on its own. pool never streams trajectory content to a hook’s stdin.
Each event adds its own fields:
The
PreToolUse and PostToolUse events for one call share tool_call_id, not event_id.
Output decision
On exit 0, non-emptystdout is parsed as a decision object. All fields are optional:
Decision fields
decision: "block" with a reason works on any event that can be blocked. The remaining fields apply per event:
A block or continue decision from a
PreCompact or SessionStart hook is ignored, and the model is told.
Use the decision field names exactly as shown. pool ignores fields it does not recognize, so well-formed JSON with a misspelled field can produce no error and have no effect.
How hooks combine
- Hooks for an event run sequentially in configuration order, never in parallel. Rewrites thread from hook to hook: each hook sees the previous hook’s validated rewrite.
- If any hook denies, the event is blocked. Deny reasons combine in configuration order. Later hooks still run, but their rewrites are not applied after a deny. Injected context still accumulates.
- A hook that times out, cannot start, exits with an unexpected code, or prints invalid JSON is skipped: the event proceeds without it and the model is told the hook failed. A failed hook never blocks anything.
- A
PreToolUserewrite that fails the tool’s argument validation blocks the call. The tool does not run with the model’s original arguments, and the model is told why. - Rewritten or injected text must be valid UTF-8. Invalid text is rejected, the original is kept, and the model is told.
Configuration reference
Define hooks under ahooks: key in settings.yaml, at any of these levels:
- Personal defaults:
~/.config/poolside/settings.yaml - Shared, project-specific:
<project-path>/.poolside/settings.yaml - Personal, project-specific:
<project-path>/.poolside/settings.local.yaml - The
--settingsflag, available onpool acponly, as the most specific level
command: Required. Runs through/bin/sh -con Unix orcmd /con Windows.matcher: Provide this field for every event. It selects tools onPreToolUseandPostToolUse. Other events ignore it, so usematcher: "*". Matcher forms:""or"*": Any tool- A bare name such as
shell: Exact match shell|bash: Pipe-separated list of exact names- Anything else: A regular expression. An invalid pattern disables the hook with a warning when settings load.
name: Optional. It labels the hook in logs and in what the model sees, and it identifies the hook when settings levels combine, so a more specific level can replace it. Without a name, the label falls back to the command’s filename.timeout: Optional per-hook limit in seconds. The default is 60. On timeout, the hook’s whole process group is killed.
Tool names and arguments
Amatcher selects a tool by name, and your script then reads fields of tool_input, which holds exactly the arguments the model passed to that tool. If your script reads a key that the tool does not have, the hook silently does nothing, so check the key names first. Tool names are the ones you use in the tools: section of Permissions, and the most commonly guarded tools take these arguments:
To see the exact payload for any other tool, log one event and read it, as in Inspect a payload.
Limit Stop hook continuations
Set stop_hook_max_continuations inside the hooks: section, alongside the event names, to cap how many times in a row Stop hooks can make the agent continue. When it is unset, continuations are unlimited, so set it whenever a Stop hook can return continue: true. Setting it to 0 refuses every continuation, and negative values are rejected by the settings schema. The counter resets each time you send a real prompt, and the most specific settings level that sets a value wins. For a Stop hook that pairs with this cap, see Keep a turn going one time.
What you and the model see
Silence is the default. A hook that changes nothing leaves no trace in the conversation. Rewrites, blocks, continuations, and failures add a<hook> tag or reminder for the model that names the hook and event.
Hook notices and reminders do not appear as standalone messages in the terminal UI. They are recorded in the session trajectory and debug log. See Get debug logs and View the trajectory.
Behavior worth knowing when you review a session:
- When a
PreToolUsehook rewrites arguments, the conversation and trajectory keep the model’s original arguments. The arguments the tool actually ran with appear inside the hook tag. - When a
PostToolUsehook rewrites output, the rewritten text is the tool result, with a tag naming the hook appended to it. - The prompt for a
Stopcontinuation is the hooks’additional_context, joined with newlines when several hooks contribute, or a default line naming the hook when none provide it. - Context injected by a
PreCompacthook is added after compaction runs, so it survives compaction verbatim.
Limits
- Hook text shown to the model is capped at 32 KiB and truncated with a marker beyond that. Prompt rewrites are not capped.
- A hook can print up to 1 MiB on
stdout. More than that fails the hook. - Block reasons on
stderrare kept up to 64 KiB.
Security
Hook commands run whereverpool runs, with the same user permissions and no additional sandboxing, automatically on every matching event. There is no approval prompt for hook execution. Hooks also fail open: a hook failure does not stop the event. Treat the hooks: section like a shell startup file such as .bashrc:
- Review the
hooks:section and every referenced script before you runpoolin a cloned repository. A repository’s.poolside/settings.yamlcan define hooks. - Hooks receive tool arguments, tool output, and your prompts on
stdin, and a hook can do anything your shell can, including sending that data over the network. - If you ask the agent to write a hook for you, review the script and the settings change before enabling them.
Examples
These additional examples usejq and a POSIX shell. Save each script in .poolside/hooks/, make it executable with chmod +x, and register it under the matching event in settings.yaml with the absolute path to the script, as shown in Configure your first hook.
Block a privileged command
APreToolUse hook with matcher: "shell" that blocks any shell command containing sudo:
.poolside/hooks/block-sudo.sh
sudo command is blocked before it runs and before any approval prompt appears. The model reads the stderr message as the tool result and can try a different approach. Any other shell command passes through untouched.
Redact tool output
APostToolUse hook with matcher: "shell" that rewrites the tool output the model reads. It stays silent when there is nothing to redact:
.poolside/hooks/redact.sh
Keep a turn going one time
AStop hook that asks the agent to keep working exactly one time, using a marker file. Pair it with stop_hook_max_continuations as a second guard:
.poolside/hooks/drive-once.sh
Inspect a payload
Any hook that writes the event JSON to a file, so you can read the exact keys a tool sends. Register it on the event you want to inspect:.poolside/hooks/log-payload.sh
Inject context at session start
ASessionStart hook that announces a workspace policy:
.poolside/hooks/announce-policy.sh