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

IntegrationHook surfaceCommand policyRuntime network/filesystem for allowed shell commandsPreflights file/network tool inputsMain caveat
Claude CodePreToolUse for BashDenies blocked commands or rewrites allowed commands to fence -c ...Yes, for hooked Bash commandsNoCovers shell tool calls, not native editor or agent file operations
CodexPreToolUse for Bash and apply_patchDenies 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 disabledNoDefault Codex sandbox blocks nested Fence proxy binds; trust via /hooks; incomplete unified_exec interception
CursorpreToolUse for ShellDenies blocked commands or rewrites allowed commands to fence -c ...Yes, for hooked Shell commandsNoCovers Cursor shell tool calls, not arbitrary IDE behavior
OpenCodetool.execute.before plugin for bashDenies blocked commands or rewrites allowed commands to fence -c ...Yes, for hooked bash commandsNoUser-typed ! commands bypass the plugin
Hermes Agentpre_tool_call for terminal, write_file, patch, web_extractDenies blocked terminal commandsNo, hook mode is intent-onlyYes, for declared write paths and URLsThe hook checks declared tool inputs; wrap Hermes for traffic-time enforcement
Windsurf Cascadepre_run_command, pre_write_codeDenies blocked terminal commandsNo, Windsurf hooks do not support command rewritingYes, for declared write pathsThe 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 toolFence policy domainReads
terminalcommand.deny / command.allowtool_input.command
write_filefilesystem.allowWrite / denyWrite and dangerous-file protectiontool_input.path
patchfilesystem.allowWrite / denyWrite and dangerous-file protectiontool_input.path
web_extractnetwork.allowedDomains / deniedDomainstool_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 eventFence policy domainReads
pre_run_commandcommand.deny / command.allowtool_info.command_line
pre_write_codefilesystem.allowWrite / denyWrite and dangerous-file protectiontool_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.