Docs › Coding tools' hooks: what was checked, where and when
Coding tools' hooks: what was checked, where and when
sparehand agent install TOOL connects a coding tool through its own hook where it has one (AI coding agents is the guide). Each tool has its own file, input and answer, and they change. This page records what the connection in lib/agent-tools.mjs and lib/agent-hook.mjs relies on, the page it was read from and the day, so the next check is a matter of opening the same pages. sparehand agent status prints the version and date from here.
Last full check: 2026-11-10.
The tools
"Changes" means the hook hands back a new command, so the heavy check runs as sparehand run --or-local … without the model doing anything. "Redirects" means the tool's hook can only refuse a command (or changing one would also approve it): the hook refuses the heavy command once and gives the model the Sparehand command to run instead. "Note" means there is no hook to use, and the connection depends on the model following a note in its instructions.
| Tool | Connection | File Sparehand writes | Checked against |
|---|---|---|---|
| Claude Code | hook, changes | ~/.claude/settings.json (hooks.PreToolUse) | 2.1.317, documentation |
| Gemini CLI | hook, changes; plus a note | ~/.gemini/settings.json (hooks.BeforeTool), ~/.gemini/GEMINI.md | 0.46.0, documentation; the settings file loads in 0.46.0 |
| Cursor | hook, changes | ~/.cursor/hooks.json (hooks.preToolUse) | 3.16.17, documentation |
| Codex CLI | hook, redirects; plus a note | ~/.codex/hooks.json (hooks.PreToolUse), ~/.codex/AGENTS.md | 0.162.0-alpha, documentation and its hook schema; Codex 0.162.0-alpha lists the installed hook without warnings |
| GitHub Copilot CLI | hook, changes; plus a note | ~/.copilot/hooks/sparehand.json, ~/.copilot/copilot-instructions.md | documentation only |
| GitHub Copilot coding agent (on GitHub) | not connected | none: it reads hooks only from .github/hooks/ on the repository's default branch | documentation only |
| Windsurf (Devin Local, Devin CLI) | hook, changes; plus a note | ~/.config/devin/config.json (hooks.PreToolUse), ~/.codeium/windsurf/memories/global_rules.md | documentation only |
| OpenCode | hook (a plugin), changes; plus a note | ~/.config/opencode/plugins/sparehand.js, ~/.config/opencode/AGENTS.md | 2.2.7 and 1.18, documentation and source |
| Kiro | hook, redirects (CLI only); plus a note | ~/.kiro/hooks/sparehand.json, ~/.kiro/steering/sparehand.md | CLI 3 and IDE 1.0, documentation only |
| Cline | note | ~/Documents/Cline/Rules/sparehand.md | documentation only |
"Documentation only" means the tool wasn't installed on the computer where this was checked, so nothing confirmed that the program accepts the file. None of the hooks was watched changing a command inside a signed-in session of its tool on that day; sparehand agent test TOOL shows what the installed hook answers, which is the half Sparehand controls.
What each hook relies on
Claude Code
- Source:
https://code.claude.com/docs/en/hooks(read 2026-11-10). - Entry:
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": …, "timeout": 10 }] }underhooks.PreToolUse;timeoutis in seconds. - Input:
tool_nameisBash;tool_inputhascommand, and optionallytimeout(milliseconds) andrun_in_background;cwd. - Answer:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":{…}}}.updatedInputreplaces the whole input, so every field goes back. NopermissionDecision: the person's allow and deny rules are then applied to the new command. - Timeout: the hook raises
tool_input.timeoutby up to 5 minutes (10 minutes at most). - No answer and exit 0 leaves the command alone. Exit 2 would block it; the hook never uses it.
- Known limit: in auto mode Claude Code can refuse a call whose input a hook changed without a decision; the documentation exempts command hooks from settings files in interactive sessions.
Gemini CLI
- Sources:
docs/hooks/index.md,docs/hooks/reference.md,docs/hooks/writing-hooks.md,docs/reference/configuration.mdanddocs/tools/shell.mdinhttps://github.com/google-gemini/gemini-cli(main, read 2026-11-10). - Entry:
{ "matcher": "run_shell_command", "hooks": [{ "name": "sparehand", "type": "command", "command": …, "timeout": 10000 }] }underhooks.BeforeTool;timeoutis in milliseconds. - Input:
tool_nameisrun_shell_command;tool_input.command;cwd. - Answer:
{"hookSpecificOutput":{"hookEventName":"BeforeTool","tool_input":{…}}}, merged over the model's arguments. - Timeout: the shell tool takes no timeout argument, so there is none to raise.
- Hooks are on by default (
hooksConfig.enabled). Anything on stdout that isn't the one JSON object breaks the answer; the hook prints nothing else. - Not documented: whether the person's approval is asked for the changed command or the first one.
Cursor
- Sources:
https://cursor.com/docs/hooks,https://cursor.com/docs/reference/third-party-hooks,https://cursor.com/docs/cli/changelog(read 2026-11-10). - File:
{ "version": 1, "hooks": { "preToolUse": [{ "command": …, "matcher": "Shell", "timeout": 10 }] } };timeoutis in seconds. A user hook runs from~/.cursor/, so the command is an absolute path. - Input:
tool_nameisShell;tool_inputhascommandandworking_directory;cwd,workspace_roots. - Answer:
{"updated_input":{…}}, the whole input with the new command. Nopermission. beforeShellExecutioncan only allow, deny or ask, so it isn't used.- A hook that fails or times out lets the command through. An answer that isn't valid JSON blocks it; the hook prints either its one JSON object or nothing.
- Cursor also reads Claude Code's
~/.claude/settings.jsonhooks (itsBashis Cursor'sShell) while "Include third-party plugins, skills and other configs" is on. A command that already runs through Sparehand is left alone, so two hooks don't wrap it twice. - Not documented: whether
updated_inputreplaces or merges; whether it is honoured in thecursor-agentCLI (its changelog sayspreToolUsefires there); the default timeout.
Codex CLI
- Sources:
https://developers.openai.com/codex/hooks,https://developers.openai.com/codex/config-reference,https://developers.openai.com/codex/sandbox, andcodex-rs/hooks/schema/generated/pre-tool-use.command.{input,output}.schema.jsoninhttps://github.com/openai/codex(read 2026-11-10). - File:
~/.codex/hooks.json, Claude Code's shape:{ "matcher": "^Bash$", "hooks": [{ "type": "command", "command": …, "timeout": 10 }] }underhooks.PreToolUse;timeoutin seconds. Hooks are on by default ([features] hooks). - Input:
tool_nameisBash;tool_input.command;cwd. - Answer, by default:
permissionDecision: "deny"with the Sparehand command inpermissionDecisionReason. Codex accepts a changed command (updatedInput) only together withpermissionDecision: "allow", and nothing says whether that "allow" also skips the person's approval.SPAREHAND_AGENT_CODEX=rewriteuses it anyway. - Trust: Codex skips a new or changed hook until the person trusts it (
/hooksin Codex; it prints a warning at startup). Until then only the note works. Sparehand doesn't write the trust itself. - Network: the default sandbox (
workspace-write) has no network, which reaching a worker needs:[sandbox_workspace_write] network_access = truein~/.codex/config.toml. Without it the run stays on this computer (or Codex asks for approval). - Checked with the program: Codex 0.162.0-alpha's own hook listing (its app server's
hooks/list) reads the installed file as onepreToolUsecommand hook with matcher^Bash$, 10 seconds, status "untrusted", no warnings and no errors.
GitHub Copilot CLI and coding agent
- Sources:
https://docs.github.com/en/copilot/reference/hooks-configuration,https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks,https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-hooks,https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks(read 2026-11-10). - File: any
*.jsonin~/.copilot/hooks/:{ "version": 1, "hooks": { "preToolUse": [{ "type": "command", "bash": …, "matcher": "bash", "timeoutSec": 10 }] } }. - Input:
toolNameisbash;toolArgsholds the arguments, as a JSON string in the documentation's example (the hook reads a string or an object);cwd. - Answer:
{"modifiedArgs":{…}}, "substitute tool arguments to use instead of the originals". - A hook that exits with an error makes Copilot refuse the command, so the hook's command first checks that Sparehand is still there and otherwise exits 0 without a word. A timeout lets the command through.
- The coding agent on GitHub reads hooks only from
.github/hooks/*.jsonon the repository's default branch, and runs in GitHub's own sandbox: there is nothing to install on this computer, and no worker to reach from there. - Not documented: the fields of
modifiedArgsfor the shell tool (taken to be the same astoolArgs), and whether it needs apermissionDecisionnext to it (none is sent).
Windsurf
- Sources:
https://docs.windsurf.com/windsurf/cascade/hooks(now leads tohttps://docs.devin.ai/desktop/devin-local/customizations),https://docs.devin.ai/cli/extensibility/hooks/overview,https://docs.devin.ai/cli/extensibility/hooks/lifecycle-hooks,https://docs.devin.ai/desktop/cascade(read 2026-11-10). - Windsurf's Cascade agent is described as legacy ("no longer starts new Cascade conversations"), and the reference of its
pre_run_commandhook is no longer published. Its successor, Devin Local (and Devin CLI), has a hook in Claude Code's shape, which is what Sparehand installs. The note stays in Cascade's global rules for older versions. - File:
"hooks"in~/.config/devin/config.json:{ "PreToolUse": [{ "matcher": "^exec$", "hooks": [{ "type": "command", "command": …, "timeout": 10 }] }] };timeoutin seconds. - Input:
tool_nameisexec;tool_input.command. No folder: the project is inDEVIN_PROJECT_DIR. - Answer:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":{"command":…}}}, merged into the arguments. - A hook that fails or times out is logged and doesn't block.
- Not documented: a timeout argument of
exec; whether Devin Local reads Cascade's global rules.
OpenCode
- Sources:
https://opencode.ai/docs/plugins,https://opencode.ai/docs/build/plugins,https://opencode.ai/docs/build/plugins/migrate-v1,https://opencode.ai/docs/tools, and inhttps://github.com/anomalyco/opencode:packages/plugin/src/promise/{plugin,tool}.ts,packages/core/src/plugin/{module,source-directory}.ts,packages/core/src/tool.ts(dev), andpackages/opencode/src/plugin/index.ts(v1) (read 2026-11-10). - File: one plugin,
~/.config/opencode/plugins/sparehand.js, without imports from OpenCode (so it needs no package):export default { id, setup, server }. - OpenCode 2:
setup(ctx)registersctx.tool.hook("execute.before", …); for the toolshellit replacesevent.input(command,workdir,timeoutin milliseconds,background). - OpenCode 1 (1.18.29 or newer):
server()returns"tool.execute.before"; for the toolbashit changesoutput.argsin place. - Timeout: raised by up to 5 minutes, as in Claude Code; a command with no time limit keeps none.
- The plugin asks
sparehand agent hook --for opencode(throughnode:child_process), so the rules are the same as everywhere else. - From the source, not the documentation: that a plain object without
Plugin.defineis accepted (definereturns its argument; the loader checks foridandsetup), and thatnode:child_processis available to a plugin.
Kiro
- Sources:
https://kiro.dev/docs/hooks,https://kiro.dev/docs/hooks/types,https://kiro.dev/docs/hooks/actions,https://kiro.dev/docs/cli/v3/hooks-migration,https://kiro.dev/docs/steering(read 2026-11-10). - File: any
*.jsonin~/.kiro/hooks/(read by Kiro CLI 3; the IDE has no folder for all projects):{ "version": "v1", "hooks": [{ "name": "sparehand", "trigger": "PreToolUse", "matcher": "shell", "action": { "type": "command", "command": … }, "timeout": 10, "enabled": true }] };timeoutin seconds. - Input:
tool_nameisexecute_bash(execute_pwshon Windows);tool_input;cwd. - Answer: none can change a command. Exit 2 blocks the call and hands stderr to the model, so the hook writes the Sparehand command there.
- The note is a steering file with
inclusion: always, which the IDE and the CLI both read. - Not documented: the name of the command's field in
tool_input(taken to becommand; with another name the hook does nothing and the note is what works).
Cline
- Sources:
https://docs.cline.bot/customization/hooks,https://docs.cline.bot/sdk/plugins,https://docs.cline.bot/getting-started/config,https://docs.cline.bot/customization/cline-rules,https://docs.cline.bot/customization/clineignore, andsdk/examples/hooks/README.mdinhttps://github.com/cline/cline(read 2026-11-10). - Note only. In the VS Code extension a hook can't change a command, and cancelling one stops the whole task. The CLI's hooks and plugins can replace a tool's input, but the shell tool's (
run_commands) input isn't documented, so there is nothing to build on yet. - The note is a rules file in
~/Documents/Cline/Rules/.
Checking again
- Open each source above and compare the entry, the input and the answer with what is written here.
lib/agent-tools.mjs(HOOKS) holds the entries,lib/agent-hook.mjs(FORMATS) the inputs and answers. - With a tool installed, connect it in a throwaway home folder (
HOME=/tmp/try sparehand agent install TOOL) and start the tool there: it should load the file without a warning. For Codex, its hook listing shows how it read the file. sparehand agent test allshows what every installed hook answers.- Update the versions and dates in
TOOLS(lib/agent-tools.mjs) and on this page.