Hooks
A hook is a shell command Polyglot runs at a specific point in the agent loop. It can
block an action or add context to a prompt. Hooks let you enforce local
policy - a command allowlist, a “don’t touch main” rule, a linter gate - without
writing a plugin.
{ "hooks": { "preToolUse": [ { "command": "deny-dangerous-commands.sh", "tools": ["bash"] } ], "userPromptSubmit": [ { "command": "echo '{\"additionalContext\":\"The repo is frozen this week.\"}'" } ] }}The three events
Section titled “The three events”| Event | Fires | A hook can |
|---|---|---|
preToolUse |
a tool call is about to run - after the permission gate allows it | block the call |
postToolUse |
a tool result is about to reach the model - after secret scanning | block the result |
userPromptSubmit |
a message is about to start a turn | block the turn, or append context to the message |
preToolUse and postToolUse run for sub-agents too.
preToolUse hooks can only tighten - they run after the gate has already allowed
the call, so a hook can’t grant something the gate denied.
The contract
Section titled “The contract”The hook is run with the working directory as cwd and POLYGLOT_HOOK_EVENT in its
environment, and gets a JSON payload on stdin:
// preToolUse / postToolUse{ "event": "preToolUse", "cwd": "…", "toolName": "bash", "toolInput": { "command": "rm -rf /" } }// postToolUse also has: "resultText", "isError"// userPromptSubmit{ "event": "userPromptSubmit", "cwd": "…", "prompt": "…" }Exit code:
| Code | Meaning |
|---|---|
0 |
proceed |
2 |
block - stderr (or the JSON reason) is what the model / user sees |
| anything else | non-blocking error - stderr is surfaced as a warning, the action proceeds |
Optional stdout JSON (any subset):
{ "decision": "block", "reason": "rm is not allowed here", "additionalContext": "…" }decision: "block"blocks even on exit 0.additionalContext(only useful foruserPromptSubmit) is appended to the message inside a<context>block. Multiple hooks’ contexts are concatenated.
A simple linter-style hook needs none of this - just exit 2 when it wants to stop
something.
Example: block rm in bash
Section titled “Example: block rm in bash”#!/usr/bin/env bashcmd=$(jq -r '.toolInput.command // ""')case "$cmd" in *" rm "*|"rm "*) echo "rm is disabled by a hook - delete files yourself if you mean it" >&2 exit 2 ;;esac{ "hooks": { "preToolUse": [ { "command": "./hooks/no-rm.sh", "tools": ["bash"] } ] } }Scoping and timeouts
Section titled “Scoping and timeouts”HookSpec field |
|
|---|---|
command |
the shell command (run via bash -c, or PowerShell on Windows) |
tools |
glob-matched tool names this hook fires for (preToolUse / postToolUse only). Unset = every tool. e.g. ["bash", "web_*"] |
timeoutMs |
kill the hook after this many ms (default 5000) |
A hook that times out or crashes is treated as a non-blocking error - Polyglot warns and proceeds. Hooks are for convenience and soft policy; anything that must fail closed belongs in a custom permission gate, not a shell hook.
Security: project hooks are off by default
Section titled “Security: project hooks are off by default”Hooks in a project’s .polyglot/settings.json are ignored unless your global
~/.polyglot/settings.json sets:
{ "hooks": { "allowProjectHooks": true } }Cloning a repo and running polyglot in it must not execute that repo’s shell
commands. When you do opt in, global and project hook lists are concatenated (global
first).
POLYGLOT_NO_HOOKS=1 disables all hooks for a run. /status shows the configured
counts (hooks: preToolUse(1) userPromptSubmit(1)). Every block is recorded in the
audit log as a hook_blocked entry.