Agent Hooks
July 12, 2026 ยท View on GitHub
Hook-based wrapping exists for environments where Fence cannot transparently enforce argv-aware command policy on every descendant process after an agent is already running. Instead of trying to catch child execs after the fact, Fence uses an agent's hook or plugin system to inspect each declared tool call before the agent runs it.
Prefer whole-agent wrapping when possible. It is the stronger
isolation model for filesystem and network policy. Hooks are most useful when
you also need multi-token command denies like git push, gh repo create, or
npm publish to apply to tool-issued shell commands on macOS, or on Linux when
command.runtimeExecPolicy: "argv" is not enabled.
Capability Matrix
| Integration | Hook surface | Command policy | Runtime network/filesystem for allowed shell commands | Preflights file/network tool inputs | Main caveat |
|---|---|---|---|---|---|
| Claude Code | PreToolUse for Bash | Denies blocked commands or rewrites allowed commands to fence -c ... | Yes, for hooked Bash commands | No | Covers shell tool calls, not native editor or agent file operations |
| Codex | PreToolUse for Bash and apply_patch | Denies blocked commands (intent-only by default); optional --wrap rewrites to fence -c ... | Only with --wrap / FENCE_CODEX_WRAP=1, and only when Codex's own sandbox is disabled | No | Default Codex sandbox blocks nested Fence proxy binds; trust via /hooks; incomplete unified_exec interception |
| Cursor | preToolUse for Shell | Denies blocked commands or rewrites allowed commands to fence -c ... | Yes, for hooked Shell commands | No | Covers Cursor shell tool calls, not arbitrary IDE behavior |
| OpenCode | tool.execute.before plugin for bash | Denies blocked commands or rewrites allowed commands to fence -c ... | Yes, for hooked bash commands | No | User-typed ! commands bypass the plugin |
| Hermes Agent | pre_tool_call for terminal, write_file, patch, web_extract | Denies blocked terminal commands | No, hook mode is intent-only | Yes, for declared write paths and URLs | The hook checks declared tool inputs; wrap Hermes for traffic-time enforcement |
| Windsurf Cascade | pre_run_command, pre_write_code | Denies blocked terminal commands | No, Windsurf hooks do not support command rewriting | Yes, for declared write paths | The hook can block declared actions but cannot sandbox allowed commands |
The important distinction is whether the agent lets the hook modify the command
before execution. Claude Code, Cursor, and OpenCode support that by default, so
Fence can turn an allowed shell tool call into fence -c "...". That nested
Fence process is what applies runtime filesystem and network policy to the
command. Codex supports the same rewrite API, but defaults to intent-only
because Codex usually runs tool commands inside its own sandbox.
Some hook systems only let a pre-hook allow or block an action, usually via an exit code or a block response. Hermes, Windsurf, and default Codex are in this category: Fence can block denied commands, write paths, or URLs, but it cannot sandbox an allowed command unless rewriting is available and safe to use.
How It Works
For shell-command rewriting hooks, the Fence helper decides per invocation whether to:
- Deny the command if it violates Fence command policy. The hook returns an error and the agent never runs the command.
- Rewrite the command to run through
fence -c "...", when the integration supports command mutation. The shell execution then happens inside the sandbox.
Commands that already violate Fence command policy are denied directly at hook
time instead of being rewritten to a nested fence -c ... invocation.
If the agent is already running inside Fence, the helper avoids launching a second nested sandbox and only applies Fence's command policy at hook time.
Shell Command Rewriting
Claude Code, Cursor, and OpenCode let Fence replace an allowed shell command
with fence -c "...". That means the shell command itself runs inside Fence,
so runtime filesystem and network policy apply to the command after the hook
allows it. Codex can do the same when you opt in with --wrap (see below), but
defaults to intent-only deny checks.
Claude Code
Claude Code uses PreToolUse for Bash and calls
fence --claude-pre-tool-use:
fence hooks print --claude
fence hooks install --claude
fence hooks uninstall --claude
Default file: ~/.claude/settings.json.
Codex
Codex (ChatGPT Codex app and CLI) uses PreToolUse for Bash and
apply_patch (matcher also accepts Edit / Write aliases) and calls
fence --codex-pre-tool-use:
fence hooks print --codex
fence hooks install --codex
fence hooks install --codex --template code
fence hooks uninstall --codex
Default file: ~/.codex/hooks.json. Override with --file for a project-local
./.codex/hooks.json.
After install, open /hooks in Codex and trust the Fence command. Untrusted
hooks are skipped. For automation that already vets hook sources outside Codex,
Codex also supports --dangerously-bypass-hook-trust.
Intent-only by default. Codex usually runs tool commands inside its own
sandbox (Seatbelt on macOS, etc.). Nested fence -c ... then fails when Fence
tries to bind its local HTTP proxy (listen tcp 127.0.0.1:0: bind: operation not permitted). So the default Codex hook only denies commands that violate
Fence command policy and leaves allowed commands unchanged.
Optional wrap mode โ only when Codex's own sandbox is disabled (for
example danger-full-access):
fence hooks install --codex --wrap
# or, without reinstalling:
FENCE_CODEX_WRAP=1
With --wrap / FENCE_CODEX_WRAP=1, allowed commands are rewritten to
fence -c "..." like Claude/Cursor. Do not enable wrap under normal sandboxed
Codex sessions.
Note
Codex shell interception is incomplete for unified_exec. Codex's
PreToolUse hook covers simple Bash tool calls and apply_patch, but not
every shell path (notably richer unified_exec streaming). WebSearch and
other non-shell / non-MCP tools are also not intercepted. The ChatGPT Codex
desktop app cannot be whole-agent wrapped with fence -- codex; use hooks
for command denies, and treat wrap mode as an advanced opt-in only.
Cursor
Cursor uses preToolUse for Shell and calls
fence --cursor-pre-tool-use:
fence hooks print --cursor
fence hooks install --cursor
fence hooks uninstall --cursor
Default file: ~/.cursor/hooks.json.
Cursor may also run Claude Code hook commands if Claude settings are present. Fence handles either Cursor or Claude hook payloads.
OpenCode
OpenCode loads plugins from npm packages listed in its plugin array, so the
Fence integration ships as the
@fencesandbox/opencode-fence
plugin. It hooks tool.execute.before for the bash tool and calls
fence --opencode-pre-tool-use:
fence hooks print --opencode
fence hooks install --opencode
fence hooks uninstall --opencode
Default file: ~/.config/opencode/opencode.jsonc if it exists, otherwise
~/.config/opencode/opencode.json (created on first install). Override with
--file to target a project-local opencode.{json,jsonc}.
install --opencode only adds @fencesandbox/opencode-fence to the plugin
array; OpenCode's npm-package plugin loader does not accept options, so
--settings and --template are not supported with --opencode. To pin a
specific config or template, write a local plugin shim under
~/.config/opencode/plugins/ that constructs FencePlugin({...}) directly.
See the plugin's
README.
Note
OpenCode !-prefixed commands bypass the plugin. OpenCode's plugin
lifecycle currently does not fire tool.execute.before for commands the
user types directly into the TUI with the ! prefix, so those bypass the
Fence plugin even when installed. Whole-agent wrapping
(fence -t code -- opencode) still applies its filesystem and network
policy to those commands; only multi-token command denies are missed for
the ! path.
Intent/Preflight Hooks
Hermes and Windsurf expose hooks for declared tool inputs. Fence can block
commands, write paths, or URLs that violate policy, but allowed actions do not
run inside a nested fence -c ... sandbox unless the whole agent is also
wrapped.
Hermes Agent
Hermes Agent has a YAML-declared shell-hook system (~/.hermes/config.yaml)
that pipes JSON to a subprocess on stdin and reads JSON on stdout, so the
Fence integration ships as the fence binary itself, no separate package.
It registers pre_tool_call hooks for Hermes' terminal, write_file,
patch, and web_extract tools and calls fence --hermes-pre-tool-use:
fence hooks print --hermes
fence hooks install --hermes --template hermes
fence hooks install --hermes --settings ./fence.json
fence hooks uninstall --hermes
Default file: ~/.hermes/config.yaml. Override with --file to target a
project-local config or alternate profile. The hermes template is the
recommended starting point because Hermes may need provider, messaging, cache,
and ~/.hermes/** write access that plain coding-agent templates do not
include.
Unlike the shell-command integrations above, the Hermes hook surface goes beyond bash. Each Hermes tool maps to one of Fence's existing config domains:
| Hermes tool | Fence policy domain | Reads |
|---|---|---|
terminal | command.deny / command.allow | tool_input.command |
write_file | filesystem.allowWrite / denyWrite and dangerous-file protection | tool_input.path |
patch | filesystem.allowWrite / denyWrite and dangerous-file protection | tool_input.path |
web_extract | network.allowedDomains / deniedDomains | tool_input.url |
Tools not in this table, including channel sends, MCP calls, subagent spawning,
memory, todos, and image or TTS generation, are passed through unmodified at the
hook layer. They do not fit Fence's filesystem, network, or command vocabulary
today. Wrap mode (fence -t hermes -- hermes) does cover their network traffic
at the proxy layer; the two modes compose.
Note
Hermes hook mode is intent-only, not traffic-enforced. Fence sees what
the agent declared it wants to do and decides against your config. It does
not sit in the syscall or HTTP path. If a tool's actual implementation does
something different from its declared arguments, the hook cannot catch that.
For traffic-time enforcement, also wrap Hermes with fence -- hermes.
Windsurf Cascade
Windsurf Cascade runs shell commands from hooks.json and blocks pre-hooks
when the hook exits with code 2. Fence registers pre_run_command and
pre_write_code hooks and calls fence --windsurf-hook:
fence hooks print --windsurf
fence hooks install --windsurf
fence hooks install --windsurf --settings ./fence.json
fence hooks uninstall --windsurf
Default file: ~/.codeium/windsurf/hooks.json. Override with --file to
target a workspace-level .windsurf/hooks.json or the JetBrains plugin's
~/.codeium/hooks.json.
Windsurf hook support maps supported events to Fence policy domains:
| Windsurf event | Fence policy domain | Reads |
|---|---|---|
pre_run_command | command.deny / command.allow | tool_info.command_line |
pre_write_code | filesystem.allowWrite / denyWrite and dangerous-file protection | tool_info.file_path |
Note
Windsurf hook mode can block, but not rewrite. Windsurf's documented
pre-hook API blocks by exit code and does not expose a response shape for
replacing tool_info.command_line. That means allowed terminal commands run
as normal Windsurf commands, not inside fence -c .... Use this integration
for command and write-path preflight checks, not traffic-time sandboxing.
Pinning a Specific Policy
By default, hook helpers resolve Fence's config at runtime the same way the CLI
does. To pin a hook to a specific file or template for --claude, --codex,
--cursor, --hermes, or --windsurf:
fence hooks install --cursor --settings /path/to/fence.json
fence hooks install --cursor --template code
fence hooks install --codex --template code
fence hooks install --codex --wrap
fence hooks install --hermes --template hermes
fence hooks install --windsurf --settings /path/to/fence.json
For --opencode, OpenCode's npm-package plugin loader does not accept options
through the plugin array. To pin a specific config or template, write a local
plugin shim under ~/.config/opencode/plugins/ that constructs the plugin
yourself:
// ~/.config/opencode/plugins/fence.ts
import { createFencePlugin } from "@fencesandbox/opencode-fence/factory";
export const Fence = createFencePlugin({
settingsPath: "/path/to/fence.json",
// or template: "code",
});
If you use this route, remove @fencesandbox/opencode-fence from
opencode.json's plugin array to avoid registering the plugin twice.
Other Agents
If your coding agent has a hook or plugin system you'd like Fence to support, please open an issue or pull request.