Guardrails and an audit trail for AI coding agents.
AgentGuard is a set of hook scripts that sit between your coding agent and your machine. One implementation covers Claude Code, Codex, Gemini CLI, Grok, Muse, and OpenCode, so every agent you run gets the same policy.
- Before a tool call runs, it blocks the classic footguns:
rm -rf ~,git push --force,git reset --hard,sudo,chmod 777, edits to.envfiles or SSH keys, and interactive commands that would hang the agent. Each block tells the agent why, and usually what to do instead. - After a tool call, it warns when command output looks like a leaked credential, and it formats and lints edited files through sley, so the agent sees findings while it is still working on the file.
- Throughout, it records every prompt, tool call, and guard decision in a
local, owner-only audit log that you can replay with
agentguard-telemetry show.
AgentGuard never edits your agent's configuration. It ships hook fragments that you merge yourself; the quickstart shows how.
This is a personal project, tuned to its author's workflow. The default policy is opinionated, so read it before you install.
| Action | What triggers it |
|---|---|
| Block | rm -rf on /, ~, $HOME, its parent, ., or ..; sudo and doas; git reset --hard; git clean -f; git push --force (--force-with-lease stays allowed), --mirror, and --prune; chmod 777; kill of PID 1, 0, or -1; interactive commands such as editors, man, top-style monitors, debuggers, and watch; edits to .env, SSH keys, *.pem, .netrc, and credential files |
| Block (opinionated) | grep -r (use rg), tree-walking find (use fd), and raw git absorb (use git absorb-and-rebase) |
| Warn | other rm -rf, kill -9, killall, pkill, world-writable chmod, a foreground sleep of 60 seconds or more (suggests wait-until, which ships with AgentGuard), credentials in command output, and repeated edits to one file (blocked after 10) |
| Remind | review the diff before committing; commit-message format and public-repo leak checks on git commit; after a push or gh pr create, the pull request is not done until its checks pass (suggests git pr-watch when it is installed) |
| After edits | format and lint edited files with sley and return findings to the agent (AGENTGUARD_POST_EDIT_LINT=0 turns lint off) |
| Record | every hook event in the audit log (AGENTGUARD_TELEMETRY=0 turns it off) |
Base Hook Policy has the full rules. Your own policy goes
in hook extensions beside the installed hooks; see examples/.
Requirements:
- Bash 4 or newer at a standard path (
/bin/bash,/usr/bin/bash,/usr/local/bin/bash,/opt/homebrew/bin/bash, or Termux's). Without one,agent-hook-pre-bashblocks every shell command; macOS's/bin/bash3.2 is too old, so install Homebrew Bash there. See Dependencies for why the path is fixed. jq.- For post-edit formatting, which is always on, and lint, which is on by
default: sley and
checkrun, plus checkrun's own
requirements,
yqand Python 3.11 or newer. Without sley, every edit returns a blockingsley command missingerror. Withoutyq, edit-time formatting and lint are skipped; the commit gate's lint check still reports the missingyq.
Install AgentGuard and its post-edit chain, then install jq and yq with
your package manager:
for repo in agentguard sley checkrun; do
curl -fsSL "https://raw.githubusercontent.com/cgraf78/$repo/main/install.sh" | bash
doneThe installer puts the hooks on disk but wires them into no agent. This
merges AgentGuard's Claude Code fragment into ~/.claude/settings.json,
keeping your existing settings and hooks and backing up the original file
the first time.
AG is the curl installer's checkout; point it at your own checkout if you
manage one:
AG=${XDG_DATA_HOME:-$HOME/.local/share}/cgraf78/checkouts/agentguard
INT=$AG/share/agentguard/integrations
mkdir -p ~/.claude
[ -f ~/.claude/settings.json ] || echo '{}' > ~/.claude/settings.json
[ -e ~/.claude/settings.json.bak ] || cp ~/.claude/settings.json ~/.claude/settings.json.bak
jq -n --arg agent claude \
--slurpfile d ~/.claude/settings.json \
--slurpfile s "$INT/claude/hooks.json" \
-f "$INT/_shared/reconcile-hooks.jq" > ~/.claude/settings.json.new &&
mv ~/.claude/settings.json.new ~/.claude/settings.jsonRerun the same commands after each update; reconciliation replaces the
previous AgentGuard hooks and leaves yours alone. The fragment calls the hooks
by bare name, so ~/.local/bin (or your BIN_DIR) must be on the PATH that
Claude Code starts with. Start a new session; when the agent tries
git push --force, the hook refuses and tells it why:
BLOCKED: git push --force rewrites remote history without a lease. Use --force-with-lease for reviewed restacks.
Other runtimes have their own fragments under
share/agentguard/integrations/<runtime>/, each with a README on how to
consume it; see Native Agent Integrations.
The curl installer keeps a durable managed checkout under $XDG_DATA_HOME
when that path is absolute, or under $HOME/.local/share otherwise, and
publishes links to its commands and manual pages. Rerunning the curl command
safely updates the clean managed checkout before republishing the links.
To choose and manage the checkout yourself instead, keep it at a stable path and run:
./install.shThe installer creates checkout-backed symlinks for every public command under
$HOME/.local/bin and for each matching manual page under
$HOME/.local/share/man/man1. Set PREFIX to relocate both trees, or set
BIN_DIR and MAN_DIR independently. Re-running the installer is safe and
retargets existing symlinks, but it refuses to replace a non-symlink path.
Moving or deleting the checkout breaks the installed links.
The direct command links preserve AgentGuard's secure-launcher boundary and
allow consumer hook extensions to remain adjacent to the invoked links. The
installer deliberately leaves lib/agentguard/ and share/agentguard/ in the
checkout and creates no completion tree. Use their absolute paths in this
checkout, or resolve them through shdeps
if you use it.
bin/agent-hook-*files are the PATH-visible hook entry points.bin/agentguard-classify-commandemits JSON command facts for non-hook policy audits that need AgentGuard's conservative shell command-word model.bin/agent-hook-telemetryrecords each hook payload in the per-session audit trail, andbin/agentguard-telemetrylists, shows, and prunes those records. See Audit Telemetry.bin/claude-session-namenames Claude transcript sessions foragent-hook-session-end-claudeand for manual transcript backfills.bin/wait-untilwaits for one concrete condition with a bounded deadline, the replacementagent-hook-pre-bashsuggests for long foreground sleeps. See Script Notes.lib/agentguard/agentguard.shis the sourceable detection API for non-hook callers. It exposesagentguard_is_session,agentguard_agent_name, andagentguard_session_idso launchers share one runtime-identity contract.lib/agentguard/hook-helpers.shis the hook-runtime API used by hook entry points and hook extensions. Sourced extensions can readAGENTGUARD_CMD_TRIMMED,AGENTGUARD_CMD_LINE1,AGENTGUARD_EDIT_FILES, andAGENTGUARD_EDIT_FILEafter the matching parser helper runs.lib/agentguard/watch.shandlib/agentguard/watch-github.shtrack watch obligations: work an agent started (pull-request checks, for one) that a watcher such asgit pr-watch --waitshould follow to the end. Hook extensions record and re-arm wrapped commands through their documented helpers;docs/watch-protocol.mddescribes the state layout, the rules, and the gaps.share/agentguard/shell.shis a stable no-op shell loader for integration harnesses that source each dependency's shell API uniformly.share/agentguard/integrations/contains canonical native hook fragments for Claude Code, Codex, Gemini CLI, Grok, and Muse, plus the OpenCode runtime adapter.agent-hook-pre-editwarns afterAGENTGUARD_EDIT_CHURN_WARNedits to a file and blocks afterAGENTGUARD_EDIT_CHURN_BLOCKedits. Defaults are5and10. Runagentguard-churn-bypass onfor a deliberate mid-session edit pass (writes a session-scoped marker the agent itself can manage);AGENTGUARD_EDIT_CHURN_BYPASS=1in the hook/launcher environment bypasses the same guard at launch time. Anexportin an agent tool shell cannot reach hook processes, so it never bypasses the guard.- Guard hooks (
agent-hook-pre-*) fail closed. Hosts block a tool call only on exit status 2 (OpenCode's adapter on any nonzero status) and treat every other status as a non-blocking hook error that runs the tool unguarded, so an unexpected exit status, a Bash runtime error raised inside AgentGuard's own files (even one a command substitution swallowed), or a missing library becomes a block with anAgentGuard internal errorreason. Advisory hooks (post-tool, lifecycle, prompt, Stop, notification) keep their status: they cannot undo a completed call, and blocking a prompt or a Stop would trap the session. agent-hook-pre-bashbounds its own work, because a hook that outlives the host timeout is also a non-blocking error. Commands over 256 KiB, with more than 2048 commands, or with one command of more than 2048 words are blocked, as is any command whose classification does not finish withinAGENTGUARD_CLASSIFY_TIMEOUTseconds (default 30, maximum 3600). Like the otherAGENTGUARD_*settings, the timeout is read from the hook environment, which an agent's tool shell cannot change.agent-hook-pre-bashcan guard a broad bare-Git work tree when an integration setsAGENTGUARD_PROTECTED_BARE_GIT_DIR. Optional companion settings areAGENTGUARD_PROTECTED_BARE_GIT_WORK_TREE(defaults to$HOME),AGENTGUARD_PROTECTED_BARE_GIT_ALIASES(space-separated shell variable names that should resolve to the protected Git dir),AGENTGUARD_PROTECTED_BARE_GIT_LAUNCHER(the PATH-visiblegitwrapper to model), and theAGENTGUARD_PROTECTED_BARE_GIT_{STATUS,LS_FILES,CLEAN,ADD}_MESSAGEremediation strings. The guard covers unscopedstatus,ls-files,clean, and whole-treegit add(-A,.,:/, globs);git addaccepts explicit paths without a--separator, andgit add -uis allowed because it restages tracked files only.
Source non-binary assets by their path in the checkout. If you manage AgentGuard with shdeps, resolve them through it so install locations stay under its contract:
. "$(shdeps dep-file cgraf78/agentguard lib/agentguard/agentguard.sh)"agentguard_session_id [fallback_namespace] prints a caller-supplied
AGENTGUARD_SESSION_ID first, then a native runtime id when one is available.
For detected runtimes without a native id it returns a namespaced
parent-process fallback, which is stable for the direct-call lifetime. The
optional namespace labels only a generic fallback; native ids and
runtime-specific fallbacks still win. With neither a detected runtime nor a
caller namespace, an ordinary human shell returns status 1 and prints nothing.
Hook entry points additionally have JSON payloads and therefore use their
JSON-aware session-state layer in hook-helpers.sh.
Every hook invocation is recorded, so you can audit exactly what an agent session did: each prompt, every tool call with its full input and output, every guard decision, and session lifecycle events. Records are owner-only JSON files, one per hook invocation, under:
${XDG_STATE_HOME:-~/.local/state}/agentguard/telemetry/sessions/<agent>/<session-key>/
agentguard-telemetry sessions # recent sessions across all agents
agentguard-telemetry show # timeline of the latest session
agentguard-telemetry show current # the session running this command
agentguard-telemetry show <key> --json | jq ...agent-hook-telemetry is the passive recorder that the native integrations
register for every event and tool; the other hooks add decision records.
Set AGENTGUARD_TELEMETRY=0 to disable recording. Location, schema, retention,
runtime coverage, and audit recipes are in
docs/telemetry.md.
AgentGuard publishes native, policy-free integration assets:
| Runtime | Asset |
|---|---|
| Shared lifecycle | share/agentguard/integrations/_shared/reconcile-hooks.jq |
| Claude Code | share/agentguard/integrations/claude/hooks.json |
| Codex | share/agentguard/integrations/codex/hooks.toml |
| Gemini CLI | share/agentguard/integrations/gemini/hooks.json |
| Grok | share/agentguard/integrations/grok/hooks.json |
| Muse | share/agentguard/integrations/muse/hooks.json |
| OpenCode | share/agentguard/integrations/opencode/agentguard.js |
The declarative fragments contain only AgentGuard hook activation: native event names and matchers, timeouts, runtime identity, session identity, and hook commands. They intentionally exclude permissions, models, MCP servers, UI settings, and machine policy. Consumers choose whether to enable an integration and remain responsible for merging or installing the asset without overwriting unmanaged configuration. AgentGuard never mutates an agent's configuration. Declarative consumers should apply the shared reconciler so the current provider generation can replace changed commands and retire events while preserving user hooks and runtime-owned state.
The OpenCode adapter translates OpenCode's plugin callbacks into AgentGuard's
shared hook schema. AGENTGUARD_OPENCODE_NAME can override the default
opencode identity for a compatible runtime, and
AGENTGUARD_OPENCODE_TIMEOUT_SCALE scales hook timeouts for tests. The adapter
sets AGENTGUARD_STOP_BLOCK_ENDS_TURN=1 for the Stop hook because OpenCode
does not continue a turn when that hook exits 2. Hook
executables are invoked directly with shell startup controls removed so their
documented launcher boundary remains intact.
examples/ contains a neutral environment-policy hook extension
and complete reconciliation fixtures with expected output. Repo tests load the
extension through the real pre-Bash hook, execute the provider-owned jq
filter, compare its output, and prove a second reconciliation is unchanged.
- Bash 4 or newer for hook scripts that use the command classifier. On every
platform,
agent-hook-pre-bashandagentguard-classify-commandvalidate and re-exec/opt/homebrew/bin/bash,/usr/local/bin/bash,/data/data/com.termux/files/usr/bin/bash(Termux),/bin/bash, or/usr/bin/bash, in that order, from a fixed privileged/bin/bash -pbootstrap.HOMEandPATHnever select the interpreter. Direct executable invocation through the privileged shebang is the supported security boundary, but it also requires a trusted or sanitized dynamic-loader environment. On Linux, this includesLD_PRELOAD,LD_AUDIT, andLD_LIBRARY_PATH; platform equivalents such as macOSDYLD_INSERT_LIBRARIES,DYLD_LIBRARY_PATH, and relatedDYLD_*controls must likewise be removed or trusted before process startup. The-poption changes Bash behavior only after the dynamic loader starts it;-pcannot protect before Bash loads. Explicit interpreter invocation such asbash script-pathis outside this security boundary: Bash can run caller-controlledBASH_ENVcode that changes or forges behavior before the script's first instruction, and no shell script can undo or reliably detect those earlier effects. Rejection of an otherwise ordinary, detectable explicit invocation is best-effort only, not a security guarantee. Before candidate discovery, the fixed platform awk (/usr/bin/awk, or Termux's/data/data/com.termux/files/usr/bin/awk) must return an exact clean or dirty environment sentinel. Raw exported-function entries trigger a clean fixed-Bash environment rebuild: valid ordinary exported names and values travel as NUL-delimited records over the clean process's initial stdin, never as command-line operands, whileBASH_ENV,ENV, recursion, POSIX and compatibility controls, shell-internal state, legacy re-entry markers, and everyBASH_FUNC_*%%entry are excluded.BASH_XTRACEFDis made non-exported instead of unset so Bash cannot close its caller-owned target. Collision-free private descriptors below Bash's reserved fd 255 preserve caller stdin and stdout, including a closed stdin, while pre-source stdout carries the handshake; those descriptors close before application code runs. Exhausting that portable descriptor range fails closed. Each candidate proves Bash 4 semantics and sources the launcher in that same process with the original stdin, stdout, argv, and exit status. Both directly executed entry points exit with status 2 rather than continuing unguarded when no compatible Bash is available or a fixed inspection or scrub tool fails. jqfor hook payload parsing and JSON responses.- OpenCode supplies the Node.js runtime for its adapter. On POSIX systems, the
adapter uses Python 3 and
pswhen available to sweep an entire timed-out process session. Python runs in isolated mode from a neutral directory; direct process-group cleanup remains the fallback. cgraf78/sley, which runscgraf78/checkrun, is a hard runtime dependency for hooks that format files.agent-hook-post-editinvokes the PATH-visiblesley hook format --stdinbatch CLI (falling back tosley hook format-fileper file on older sley installs without the batch entry), thensley hook lint --stdinon every edited file, the same checks the commit gate runs, and returns findings as agent context (AGENTGUARD_POST_EDIT_LINT=0turns that lint off). Commit readiness belongs in native VCS hooks so human and agent workflows share one path; edit-time lint only reports earlier, and also covers files that no commit checks.
Optional integrations are detected at runtime: hm enables Hive Memory hook
context when its config is available through HIVE_MEMORY_CONFIG, an absolute
XDG_CONFIG_HOME, or ~/.config; sl, git, and jj enable repository
status context; term-notify-sound plays the agent-hook-stop and
agent-hook-notification terminal notifications; term-pane-mark marks the
agent's tmux pane busy from a submitted prompt or the first tool of a
notification-started turn. Background child work keeps the pane busy when the
host exposes a reliable parent task snapshot or child turn identity. The
pane terminal's foreground agent owns the mark, so launched agents cannot
clear it independently (both commands come from
tmux-tools); and
claude-templates enables a Claude-specific maintenance hook when that
command is installed.
When the host supplies session and turn IDs, busy marks carry a length-prefixed
session/turn token so a delayed Stop cannot clear newer work. The foreground-terminal
ownership check remains mandatory. Codex's shared app-server executes hooks
in a detached backend, which cannot identify an attached TUI from process
ancestry; such hooks deliberately mark no pane. Interactive codex --no-daemon
keeps the hook parent in the foreground terminal and supports this integration
without guessing a session-to-terminal association.
Grok team completion relies on its next parent Stop reporting the current
background task list; the child Stop payload has no stable task ID to match
against that list.
Claude marks each subagent and teammate turn from its SubagentStart to its
SubagentStop. Claude's task snapshots list an idle teammate as running until
it shuts down, so they only retire children whose Stop never arrived, and
Claude's idle prompt, sent once the parent and every child agent are idle,
clears any that remain. Claude runs no hook when Esc interrupts the parent, so
that turn's mark stays until a later turn ends. The same state records each
child's latest start, so hook extensions can ask whether one child is still
working through _hook_child_working (see Shared Helpers).
The launcher trust boundary includes the fixed platform Bash, awk, and env
paths and the absolute Bash candidate files above. Termux uses its
corresponding fixed paths below /data/data/com.termux/files/usr/bin; other
platforms use /bin/bash -p, /usr/bin/awk, and /usr/bin/env. The selected
fixed Bash interpreter is the bootstrap trust anchor and must be a working
Bash. Candidate entry programs reject accidental non-Bash executables
and avoid a validate-then-reopen race, but they cannot authenticate a
deliberately malicious file already installed at a trusted path. The HOME
and PATH variables cannot add candidate paths. After bootstrap,
PATH-visible dependencies such as jq and optional integration CLIs remain
caller-trusted.
Run test/agentguard-test to execute repo-owned hook suites. Behavioral
coverage for hook entry points lives here, including the protected bare-Git
guard that the command classifier exercises. Downstream consumers should keep
only install and wiring smoke tests. Consumers that need command detection
should call agentguard-classify-command rather than sourcing private
_hook_* classifier helpers. Independent suites use four workers by default;
set AGENTGUARD_TEST_JOBS to a positive integer to tune that bound for a
specific host. Hook latency budgets always run alone.
Base hooks follow the same shape:
set -u
_HOOK_SELF="${AGENTGUARD_HOOK_SELF:-$0}"
_HOOK_BIN_DIR=$(_agentguard_script_dir "${BASH_SOURCE[0]}")
source "$_HOOK_BIN_DIR/../lib/agentguard/hook-helpers.sh"
# parse input and run base logic
_hook_source_extensions
_hook_finish
The secure pre-Bash bootstrap is generated into its two entry points from
support/secure-launcher.sh.template. _agentguard_script_dir (and its
_agentguard_script_parent helper) is generated into every launcher from
support/script-resolver.sh.template. The resolver cannot be loaded from lib/
or a PATH-visible command because it is what locates lib/, and launchers
sometimes invoke hooks with a minimal PATH (the reason agent-hook-pre-bash
re-execs a modern Bash). Run support/sync-hook-bootstrap after editing the
templates; the test entrypoint runs --check against the authoritative launcher
manifest to prevent missing or stale copies.
The split between _HOOK_SELF and _HOOK_BIN_DIR is intentional: _HOOK_SELF
keeps extension discovery adjacent to the invoked symlink in ~/.local/bin,
while _HOOK_BIN_DIR resolves through that symlink to load dependency libraries.
Extension scripts are sourced, not executed:
-<agent>files (for example-claude,-codex,-gemini,-grok,-muse, or-opencode) are selected by appending the detected agent name to the hook's filename, so any runtime identity can have its own extension.-workfiles are a second, site-local extension slot, for example for an employer's policy.
Each hook emits one JSON response through _hook_finish.
hook-helpers.sh provides:
- accumulators:
_hook_block,_hook_warn,_hook_remind,_hook_context(_hook_warnand_hook_remindalso add model-visible hook context; use_hook_contextdirectly for context that should not appear as a warning or reminder in stderr) - parsers:
_hook_parse_command,_hook_parse_edit_files,_hook_parse_mcp - tool payload adapters:
_hook_tool_stdout,_hook_tool_stderr,_hook_tool_response_is_string(Claude-style objects, Codex's combined output string, and Muse's JSON envelope string, unwrapped only when the detected runtime is Muse) - Hive Memory adapters:
_hook_hm_session_start,_hook_hm_prompt_submit,_hook_hm_tool_complete,_hook_hm_stop - once markers:
_hook_prompt_cycle_reset,_hook_once_per_prompt,_hook_once_per_session - state helpers:
_hook_edit_churn_file,_hook_counter_read,_hook_counter_increment,_hook_counter_reset,_hook_now(current epoch),_hook_write_atomic - child lifecycle:
_hook_child_working <agent-id>succeeds while that child has a turn in progress in this session (a Claude child from itsSubagentStartto itsSubagentStop, as busy marks track it); missing, unreadable, or older team state, and a last start six hours old or more, read as not working - target directory setup:
_hook_cd_to_target - extension loading:
_hook_source_agent,_hook_source_work - final JSON emission:
_hook_finish
The per-session state directory is computed once at source time and refreshed
after hook JSON is read. A neutral AGENTGUARD_SESSION_ID wins when a launcher
supplies one. Managed Codex hooks prefer JSON session_id after stdin is
available, because nested Codex launches can inherit an outer
CODEX_THREAD_ID. Without JSON, Codex uses CODEX_THREAD_ID or its parent
process key; Grok uses GROK_SESSION_ID, then its nearest grok ancestor
key, then grok-$PPID; Muse uses MUSE_SESSION_ID; Claude uses
CLAUDE_CODE_CURRENT_SESSION_ID or CLAUDE_CODE_SESSION_ID; and Gemini uses
its nearest gemini ancestor key or gemini-$PPID. Any remaining hook uses
the pid of its nearest muse ancestor when that is the closest agent
process (covering id-less Muse), and otherwise falls back to $$ (for
example id-less Claude or OpenCode).
The state root itself is per-user, never a shared, predictable /tmp
directory: an absolute $XDG_RUNTIME_DIR/agentguard/hook-state when available
(a per-user directory the system clears on logout), falling back to an absolute
$XDG_STATE_HOME, then ~/.local/state, and finally a uid-scoped tmp path only
when neither an absolute XDG root nor HOME is available. Relative XDG roots
are ignored as required by the XDG base-directory contract.
A tier is used only when the hook can actually write it. A sandboxed agent can
deny writes to a tier that exists and passes test -w (Grok's Landlock
workspace profile denies $XDG_RUNTIME_DIR while hooks still see it set), so
each hook probes with a real zero-byte append and takes the first tier that
accepts it, in the fixed order above. The sandbox is fixed for a session's
lifetime, so all of a session's hooks agree on the tier, and an unsandboxed
session keeps the runtime tier as before. The $XDG_STATE_HOME and ~/.local/state tiers survive logout,
so SessionStart prunes their session directories with nothing written for
AGENTGUARD_STATE_PRUNE_DAYS (default 30; 0 disables pruning). SessionEnd
removes nothing, because resumed sessions reuse their key and expect their
markers. The tmp fallback is never pruned.
Launchers should set AGENTGUARD_NAME with AGENTGUARD_SESSION_ID when they know
the concrete agent. AGENTGUARD_SESSION_ID alone falls back to the generic
agent identity.
To add a new managed agent runtime:
- add a canonical, policy-free native fragment or runtime adapter under
share/agentguard/integrations/ - make every managed hook command set
AGENTGUARD_NAME=<agent>andAGENTGUARD_SESSION_ID=<stable-id-or-empty> - map the agent's hook payload schema into the shared
agent-hook-*scripts instead of adding policy directly to the agent config - add
-<agent>extension scripts only for behavior that is truly runtime-specific - set
AGENTGUARD_STOP_BLOCK_ENDS_TURN=1foragent-hook-stopwhen the agent logs a Stop hook's exit 2 and ends the turn anyway - update
detect.shonly when the runtime exposes reliable process or environment signals - add tests that verify native event mapping, hook environment injection, session identity, payload parsing, and referenced hook executables
- if the agent supports skills or extensions, update the gstack registration helper or document why it is unsupported
-
agent-hook-pre-bashblocks destructiverm -rftargets such as/,~,$HOME,$HOME's parent,.,.., and their/*globs, and warns on otherrm -rfusage. It also blocksgrep -r/-R/--recursive(userg), tree-walkingfind(usefd), rawgit absorb(usegit absorb-and-rebase),sudo/doas,git reset --hard,git clean -f,git push --force/-f/+refspec(--force-with-leasestays allowed),git push --mirror,git push --prune, interactive TTY commands (editors,man/info,top-style monitors, debuggers,watch;vim/vi/view/nvimare exempt with a batch flag such as-esor--headless;nvimalso supports the explicit--server VALUE --remote-expr VALUEclient form, andgdbsupports-batch/--batchand-batch-silent/--batch-silent. Values and operands that merely spell these flags do not select those modes.manis exempt with an explicitcatpager such asman -P cat,man --pager=cat, orMANPAGER=cat man). A literal command-prefixPAGER=catalso works whenMANPAGERdoes not override it; other pagers, nonemptyMANOPT, and unsupported modes stay blocked. It also blockskilltargeting PID1,0, or-1,chmod 777, Git or Sapling commit-authoring commands inside command substitutions, and the protected bare-Git scans described above. It warns onkill -9/SIGKILL,killall,pkill, and world-writablechmodmodes. It reminds agents to run a review/simplify pass and inspect the final diff before commit-class commands (metadata-only changes skip that reminder), and adds commit-message format and public-repo leak reminders forgit commit. It warns, never blocks, on a foregroundsleeptotalling 60 seconds or more (GNU operands such as300,5m,1h, or1.5m, summed), pointing atwait-untilor the runtime's background execution: a fixed sleep holds the whole agent turn, and polling the agent's own background-task output is pointless when the host notifies on completion. A sleep backgrounded with&(alone or in a backgrounded list or pipeline), a tool call the host runs in the background (Claude Coderun_in_background, Gemini CLIis_background), andsleeptext inside quotes, heredocs, orechostay silent. The check is pure Bash string work on the fast path; the background flag costs onejqcall, and only after a long sleep has matched. Background detection is a deliberate heuristic with known misses in both directions (for example,timeout 30 sleep 300still warns), acceptable because it only gates advice.wait-until --cmdpayloads get the same guards asbash -cpayloads, sincewait-untilruns them withbash -c. It warns whengit pr-watch --waitis launched detached (&, nohup, setsid, disown), and on Claude Code when it runs in the foreground withoutrun_in_background, because either way nobody receives the watcher's result;AGENTGUARD_WATCH_BLOCK=1blocks instead (see watch obligations). -
agent-hook-pre-searchdispatches file-search tool calls to agent-specific extensions and enforces nothing itself.agent-hook-pre-search-museblocks Muse file searches that do not follow symlinks; uninspectable payloads fail open. -
agent-hook-post-bashscans command stdout for high-confidence credential patterns. After a successfulgit pushorgh pr create,ready,reopen, orupdate-branch, or output showing a push summary or a new pull-request URL (pushes run through a script or terminal multiplexer), it reminds once per prompt that the pull request is not done until its checks pass on the pushed commit.git pr-open,git pr-submit,git pr-restack, andgit pr-sync-stackcount as pushes too, and so do pull-request URLs from any GitHub host. Whengit pr-watchis installed, creating a pull request records it as a watch obligation for the session, a later push orgh pr ready|reopen|update-branchre-arms it, a successfulgh pr closeorgh pr merge(with a merge method, without--auto) naming it forgets it (a reopen revives it; see the watch protocol's gaps), and the reminder recommendsgit pr-watch --wait <pull requests>; with nothing recorded, the general reminder recommendsgit pr-watch --wait <pr>too, falling back togh pr checks --watchonly withoutgit pr-watch(AGENTGUARD_PR_WATCH_HINTreplaces that advice everywhere;AGENTGUARD_WATCH=0turns obligations off). Hook extensions that unwrap commands run the same pass through_hook_gh_watch_pass. Seedocs/watch-protocol.md. Output extraction is centralized so agent-specific payload names do not leak into the base hook: it reads Claude-style output objects, Codex's combined output string, and Muse's JSON output envelope (unwrapped only when the detected runtime is Muse, whoseexit_code, or without one afailedortimed_outterminal_status, also decides whether the command failed). -
agent-hook-pre-editparses edited paths, blocks edits to secret and credential files (such as.env, SSH keys,*.pem,credentials, and.netrc), reminds once per user prompt on code/config edits to apply AGENTS.md design/workflow/code-style guidance plus any loaded language-specific rule fragments, warns or blocks repeated edits to the same file unless bypassed (agentguard-churn-bypass on, orAGENTGUARD_EDIT_CHURN_BYPASSin the hook environment), and leaves room for environment-specific generated-file or readonly-file guards. -
agent-hook-post-editformats changed files throughsley hook format --stdin, one batch per directory so repo detection matches the old per-file path. It then lints every edited file withsley hook lint --stdin, the same checks the commit gate runs, and returns findings as agent context, so a lint or schema error surfaces at edit time, including in generated or untracked files no commit checks. Lint covers whole files, so findings can predate the edit. Commit readiness and verification stay with the native commit hooks. Edit-time lint is on by default. It adds a lint run to every edit (about 0.2 s per batch, more for large shell scripts, where shellcheck dominates); setAGENTGUARD_POST_EDIT_LINT=0in the hook/launcher environment to turn it off for all file types. Formatting still runs, and the commit gate still lints. Like the churn knobs, anexportin an agent tool shell cannot reach hook processes. -
agent-hook-pre-mcpguards MCP calls: it blocks a server after three consecutive failures, warns on exactsearch_filesleaf-tool calls without a path filter, and warns once on exactknowledge_loadleaf-tool calls because large docs can consume significant context. The circuit breaker is half-open: 60 seconds after the last failure or probe, the next call goes through as a probe. The counter is per server and shared by every agent in the session. The block message says when that retry is allowed and quotes the last failing tool and the start of its error, since the blocked agent may never have seen it and repeated failures are often bad arguments rather than a down server. The expensive-call warnings are skipped for a blocked call. -
agent-hook-post-mcptracks MCP failure streaks for that circuit breaker: a success resets it, a failure (including a failed probe) counts, records the tool and error, and restarts the cooldown. -
agent-hook-session-startdetects repo type insl,git, thenjjorder, reports uncommitted changes, warns on high disk usage, and injects Hive Memory startup context whenhmis installed and configured. -
agent-hook-session-endparses session metadata for agent-specific naming or sync extensions. -
agent-hook-compacthandles PreCompact: it tells Hive Memory the host is about to drop earlier context (see delivery tracking below). -
agent-hook-prompt-submitletshm hook prompt-submithandle memory-intent reminders and context refresh decisions, then resets prompt-cycle state used by once-per-prompt guidance. The hook passes prompt text and path facts; it does not decide what should be written. -
agent-hook-stopasks Hive Memory for any pending-memory reminder, runs its extensions, checks the session's watch obligations (once per prompt, a stderr reminder naming recorded pull requests with no live watcher and no green, merged, or closed result from a check after their last push or submit; withAGENTGUARD_WATCH_BLOCK=1on Claude Code, one block per push, submit, or watcher exit instead, a stall exit never re-arming it; a session that recorded nothing pays only a directory test), and plays a terminal notification for the first Stop after a valid prompt that ends the foreground turn. A Stop an extension blocks to keep the agent working stays silent, so the Stop that later ends the turn rings although the host flags it as a continuation; other re-entrant and repeated callbacks stay silent until another valid prompt re-arms the shared AgentGuard state. A block ends the turn anyway on Codex and on hosts that setAGENTGUARD_STOP_BLOCK_ENDS_TURN=1(the OpenCode adapter does), so that Stop rings, and so does a Stop that dies first (a crashed extension or a host timeout's HUP or TERM) without a block. A SIGKILL timeout (OpenCode's) cannot ring. Codex Stop continuations are currently suppressed because affected Codex releases persist their synthetic messages with replay-invalid item IDs; prompt-submit still surfaces memory reminders before Stop. Grok Stop always emits empty JSON because Grok treats Stop context as keep-working feedback; an extension block still exits with status 2. When the host supplies a session and turn identity, completion notifications admit only the current foreground turn. Child/background prompts cannot rearm the parent's notification, and delayed Stops cannot consume a newer turn's bell. Prompt rearming and Stop publication share a bounded lock; a successful rearm finishes after the previous publisher. A timed-out or failed rearm suppresses subsequent claims until a later successful prompt; it cannot retract a notification already admitted and in flight. Legacy hosts without turn identities keep their existing prompt-cycle behavior.Tagged lifecycle notifications use Python 3's kernel file lock; Python 3 is required on hosts supplying turn identities. Without it those notifications fail silent. Kernel locks release after the final synchronous publisher exits, including forced termination; the private
attention-lockfile stays in place and must not be removed while the session is active.agent-hook-notificationplays notifications for host attention events such as permission requests.
Hive Memory integration is
deliberately centralized behind hm hook <event>.
The shell hooks pass only event facts: agent id, session id, and the best
available active path. Store affinity, project resolution, context freshness,
and refresh policy live in hm, so agent-specific or local extension files
do not need to duplicate memory policy. Clearing a pending-memory reminder
needs one more piece that AgentGuard does not ship: a launcher or wrapper that
exports HIVE_MEMORY_AGENT_ID and HIVE_MEMORY_SESSION_ID into the agent's
tool shell, matching the agent name and session key the hooks pass to
hm hook. With them, hm remember and hm note write receipts that clear the
reminder after the next successful tool event. Without them, the memory is
still written, but the reminder can repeat.
When an agent session is launched from $HOME, AgentGuard treats that as "no
active project" rather than passing home as a project hint. Explicit file paths
under $HOME still pass through, so one long-lived session can work across many
projects without collapsing context onto the home directory itself.
For Claude Code, Codex, and Muse, AgentGuard also turns on Hive Memory
delivery tracking (hm hook ... --delivery). hm then skips memories the
model can still see, for example when a session moves back to a project it
already received context for. This is safe only because every way these hosts
lose context is reported:
agent-hook-compact(PreCompact: Claude Code, Codex, and Muse) and SessionStart withsourcecompact,clear, orresumestart a new context epoch, and SessionStart re-injects. Muse compaction keeps session-start context and Muse prompt context never outlives its turn, so Muse keeps its epoch there.- A reset that fails leaves a marker so the next prompt resets first.
- AgentGuard passes each host's context lifetime (Muse drops prompt context every turn) and size limit (Claude shows at most 10,000 characters of hook context; Codex previews context above 2,500 approximate tokens; Muse drops hook output over 16 KB). Codex's default threshold uses UTF-8 bytes divided by four, rounded up. Its complete tracked context is budgeted below 9,500 bytes, leaving 500 bytes of headroom, including any context before Hive Memory. Codex still preserves oversized output in its full saved artifact; the preview alone does not count as full memory delivery.
- The size budget is exact: the host limit minus context already present, counted in the host's units (JSON-escaped response bytes on Muse), minus the tail recorded for that event. A tail is recorded only when context added after the memory block actually pushed a response over the limit; the last such overflow replaces it, capped at half the limit unless two overflows were consecutive. Compaction, clear, or resume forgets it, and so does a Muse project-change block.
- When less than 2 KB would be left, or after three overflows (counted until the next compaction, clear, or resume), the hook runs untracked, which behaves exactly as without tracking; a rewritten context whose tail cannot be measured counts as three overflows.
hmcounts a memory as delivered once it emits it within that limit. When the host will not show it after all, AgentGuard leaves a reset pending so the next prompt re-sends:_hook_finishdrops memory context that would exceed the limit and records the overflowing tail. A failed trackedhmcall, a response the hook could not write, or a prompt it blocks get the same treatment. A blocked Muse prompt is the exception: Muse prompt context never counts as delivered, so it has nothing to re-send. A pending reset is cleared once a tracked response re-sent memory context under it (even a header-only block when everything is already visible).- Accepted gap: when another plugin's hook blocks the prompt, or the host
times out the hook after
hmreturned, those memories wait for the next reset (compaction, clear, resume). Untracked prompt recall has the same gap.
Subagents, nested sessions whose session variables point at a parent, other
runtimes, and AGENTGUARD_HIVE_MEMORY_DELIVERY=0 stay untracked. So does an
hm that predates the flags; AgentGuard falls back automatically.
Claude-specific extensions auto-name untitled sessions and run the daily
claude-templates update refresh in the background.
wait-until polls exactly one condition and returns as soon as it holds:
wait-until [--timeout SECS] [--interval SECS] --file PATH
wait-until [...] --pattern REGEX --in FILE # FILE may appear later
wait-until [...] --pid PID # process has exited
wait-until [...] --cmd 'COMMAND' # bash -c COMMAND exits 0
--timeout defaults to 90 seconds, under Claude Code's default 120-second Bash
tool timeout, and --interval to 2. Longer waits need an explicit --timeout
and should run as a background tool call (run_in_background) so the agent's
tool timeout cannot kill them first. It exits 0 when the condition holds, 124
on timeout with a one-line diagnostic of the last observed state, and 2 on
usage errors. Each --cmd probe runs in its own process group, which is
stopped (TERM, then KILL after a short grace period) when the probe finishes or
its budget expires, so a hung probe cannot outlive the deadline by more than
that; a probe started at the deadline still gets two seconds to finish. --pid
decides existence from kill -0 and its error (no such process versus not
permitted), so a hidepid /proc or a missing ps cannot hide an exit, and
spots zombies through /proc on Linux or ps elsewhere; it never reports an
exit it cannot confirm. Older Bash (macOS /bin/bash 3.2) measures the
deadline in whole seconds and can end up to a second early.
claude-session-name is a PATH-visible helper used by
agent-hook-session-end-claude. It can also name older transcripts in batch:
claude-session-name <transcript_path>
MIT. See LICENSE.