wellworn

Hooks for other agents

Cursor, Windsurf and Gemini CLI all run a script before a tool call, and what each one can do with the output differs.

The Claude Code plugin prints traps into the agent's context before an install command runs. Three other clients have a comparable event, and each one handles the output differently, so the same script does not give you the same result everywhere.

ClientEventConfig fileWhat the output can do
Claude CodePreToolUsehooks/hooks.json in a plugin, or settings.jsonadditionalContext goes to the model without blocking
CursorbeforeShellExecution.cursor/hooks.jsonuser_message shows the human; the agent is told only on a deny
Gemini CLIBeforeTool.gemini/settings.jsonsystemMessage shows the human; the agent is told only on a deny
Windsurfpre_run_command.windsurf/hooks.jsonstdout shows the human with show_output; exit 2 blocks

The split matters. Claude Code is the only one of the four where an advisory hook reaches the model: the others give the model text only by refusing the tool call, which turns a warning into a wall. On Cursor, Gemini CLI and Windsurf, treat the hook as a message to the person watching, and let the agent get the same facts by calling the MCP itself, which is what the skills tell it to do.

Each config below runs a script of your own. The CI script is a working starting point: it reads names, calls GET /api/traps, and prints plain text.

Cursor

.cursor/hooks.json
{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [
      {
        "command": "./scripts/wellworn-hook.sh",
        "matcher": "npm install|pnpm add|yarn add|bun add"
      }
    ]
  }
}

The script reads the pending command as JSON on stdin, with command, cwd and sandbox fields, and writes one JSON object on stdout. To warn without stopping anything, return {"permission":"allow","user_message":"<the traps>"}. Exit code 2 blocks the command, so let the script exit 0 on every failure path or a network blip turns into a blocked install. Cursor also fires beforeMCPExecution and beforeReadFile, and hooks can live at ~/.cursor/hooks.json for every project.

Verified 2026-09-08 against cursor.com

Gemini CLI

.gemini/settings.json
{
  "hooks": {
    "BeforeTool": [
      {
        "matcher": "run_shell_command",
        "hooks": [
          {
            "name": "wellworn-traps",
            "type": "command",
            "command": "./scripts/wellworn-hook.sh",
            "timeout": 8000
          }
        ]
      }
    ]
  }
}

The matcher is a regex over the tool name, so run_shell_command catches shell calls and mcp_.* would catch MCP tools. Input arrives as JSON with tool_name and tool_input; output is one JSON object, and systemMessage puts your text in the terminal. Print nothing else on stdout: Gemini CLI treats any non-JSON on stdout as a parse failure, defaults to allowing the tool, and shows the whole output as a system message. Send debugging to stderr.

Verified 2026-09-08 against github.com

Windsurf

.windsurf/hooks.json
{
  "hooks": {
    "pre_run_command": [
      {
        "command": "bash ./scripts/wellworn-hook.sh",
        "show_output": true
      }
    ]
  }
}

Windsurf has no JSON output contract for pre-hooks: exit 0 lets the command run, exit 2 blocks it and shows stderr, and show_output decides whether the hook's stdout is displayed at all. pre_mcp_tool_use covers MCP calls. User-level hooks go in ~/.codeium/windsurf/hooks.json.

Verified 2026-09-08 against docs.devin.ai