# Hooks for Codex, Cursor, Windsurf and Gemini CLI

> One command puts a fail-closed Immiscible hook in front of Codex, Cursor, Windsurf or Gemini CLI, for one person or for every person on a machine through the agent's managed settings. Shell commands, MCP tool calls and file writes are decided by the same rules as Claude Code's.

Source: https://immiscible.fly.dev/docs/guides/coding-agent-hooks

Claude Code has its own hook and `immiscible init` installs it. The other coding agents people run at work each have a hook system of their own, and each can be made to block a call. `immiscible install` writes the hook for them:

```bash
immiscible install codex      # or cursor, windsurf, gemini
```

Each asks Immiscible before a shell command and an MCP tool call, and before a file write where the agent has that event. The request is the one the [Claude Code hook](https://immiscible.fly.dev/docs/cli.md#init) sends, so the same rule decides for every agent: a read goes ahead, an edit or a test run inside the project goes ahead once the agent is past its intern stage, a force push or a deploy waits for a person, and a secrets file leaving the machine is refused.

> **Note**
> `install` is in the next CLI release and is not yet on npm. Until it is, run it from a checkout of this repository: `node packages/immiscible-cli/bin/immiscible.mjs install codex`.

## What each agent gets

| Agent | Events | When a person must approve |
|---|---|---|
| Codex | `PreToolUse` for `Bash`, `apply_patch` (each file of a patch is checked) and MCP tools | the hook waits up to four minutes for an answer, then goes ahead or refuses |
| Cursor | `beforeShellExecution` and `beforeMCPExecution`, with `failClosed: true` | Cursor asks you, with the reasons and the approval link |
| Windsurf | `pre_run_command`, `pre_mcp_tool_use` and `pre_write_code` | refused with the approval link; run it again once approved |
| Gemini CLI | `BeforeTool` for `run_shell_command`, `write_file`, `replace`, `web_fetch` and MCP tools | the hook waits up to four minutes for an answer, then goes ahead or refuses |

Only Cursor's hooks can ask the person at the keyboard. Codex and Gemini CLI can only allow or block, so the hook holds the call while someone answers in Slack, Teams, email or the console; the hook's timeout in the agent's settings is set above the wait. Windsurf documents no hook timeout, and a hook it stops is treated as a pass, so the Windsurf hook never waits. A call that is held and not answered in time is refused with the approval link; running it again makes a new request.

Immiscible's own read-only MCP tools (`check_action_status`, `explain_decision`, `spend_summary`, `find_waste`, `unwatched_keys`) are never sent back to Immiscible. Its tools that act still are.

## For one person, or for a whole machine

```bash
immiscible install cursor --key ask_...                   # for you: ~/.cursor/hooks.json
sudo immiscible install cursor --scope managed            # for everyone on this machine
immiscible install codex --scope managed --dry-run        # show the files, write nothing
```

| Agent | `--scope user` | `--scope managed` |
|---|---|---|
| Codex | `~/.codex/hooks.json` (`$CODEX_HOME` if set) | `/etc/codex/requirements.toml` on macOS and Linux, `%ProgramData%\OpenAI\Codex\requirements.toml` on Windows |
| Cursor | `~/.cursor/hooks.json` | `/Library/Application Support/Cursor/hooks.json`, `/etc/cursor/hooks.json`, `C:\ProgramData\Cursor\hooks.json` |
| Windsurf | `~/.codeium/windsurf/hooks.json` | `/Library/Application Support/Windsurf/hooks.json`, `/etc/windsurf/hooks.json`, `C:\ProgramData\Windsurf\hooks.json`; the Devin Desktop file in the same places when it already exists, since it hides the Windsurf one |
| Gemini CLI | `~/.gemini/settings.json` | `/Library/Application Support/GeminiCli/settings.json`, `/etc/gemini-cli/settings.json`, `C:\ProgramData\gemini-cli\settings.json` (or `GEMINI_CLI_SYSTEM_SETTINGS_PATH`) |

The user scope puts the hook in `~/.immiscible`; the managed scope puts it in an `immiscible` folder beside the managed file. What the managed scope adds:

- **Codex:** `allow_managed_hooks_only = true`, so hooks from people's own settings, projects and plugins do not run, and a `[hooks]` table naming the hook's folder as `managed_dir`. Immiscible's lines sit between `# >>> immiscible` and `# <<< immiscible` markers; everything else in the file is left as written. If the file turns hooks off (`[features] hooks = false`), install refuses rather than write a hook that never runs.
- **Cursor:** enterprise hooks run before project and user hooks, and a deny from any hook wins.
- **Windsurf:** system hooks run before user and workspace hooks.
- **Gemini CLI:** `hooksConfig.enabled: true` in the system settings, which override user and project settings. A person can still list a hook by name in their own `hooksConfig.disabled`; install warns when the file it writes lists Immiscible's.

Device management tools can deploy the same files: run with `--dry-run --json` to get them, or set `IMMISCIBLE_MANAGED_ROOT` to write them under one folder for packaging.

## The agent key

The hook reads `IMMISCIBLE_URL` and `IMMISCIBLE_AGENT_KEY` from the environment first, then from `hook.env` beside it. Install writes your server's address there, and the key with `--key`. Cursor and Windsurf start hooks from the app rather than your shell, and Gemini CLI gives hooks a reduced environment, so `hook.env` is usually how the key arrives. In the user scope the file is readable by you alone. In the managed scope every person's hook reads it, so a key there stands for the machine; leave `--key` out to have each person set `IMMISCIBLE_AGENT_KEY` themselves. Until there is a key, the hook refuses every call it checks.

## Failing closed

Every installed command ends in `|| exit 2`. Codex, Windsurf and Gemini CLI block a call on exit code 2 and let it through on any other failure, so a missing `node`, a missing hook file or a crash blocks the call rather than passing it. Cursor's entry also sets `failClosed: true`, because Cursor lets a call through when a hook crashes or times out otherwise. When Immiscible cannot be reached, answers with an error or takes longer than `IMMISCIBLE_TIMEOUT_MS` (10 seconds by default), the hook refuses the call and says why. If you need to work offline, remove the hook; do not teach it to allow on error.

Each agent reads its hooks when it starts, so restart it, or open a new session, after installing. Codex runs a hook from a person's own settings only after they trust it under `/hooks`; until then it does not run at all, which is one reason to prefer the managed scope, whose hooks need no trust step.

## What has been tested

The installers are built to each vendor's published documentation: [Codex hooks](https://developers.openai.com/codex/hooks), [Cursor hooks](https://cursor.com/docs/hooks), [Windsurf Cascade hooks](https://docs.windsurf.com/windsurf/cascade/hooks) and [Gemini CLI hooks](https://geminicli.com/docs/hooks/reference/), as of October 2026. The tests run the command each agent would run, in a shell with the event on standard input as each vendor documents it, against a real Immiscible server: a force push is held, a secrets file leaving the machine is refused, and an unreachable server or a missing hook blocks the call. They do not run Codex, Cursor, Windsurf or Gemini CLI themselves, and the Windows paths are taken from the documentation without being exercised.
