# Deploy zn-gate to a whole team

Put zn-gate's hooks on every machine through each agent's managed configuration: Claude Code managed settings, Cursor enterprise hooks and Codex requirements.toml, and how to check a machine.

Source: https://usezn.com/docs/team/managed-install/

This page is for the person who sets up laptops for a team. Instead of asking each developer to run `zn hooks install`, you place one configuration file per agent in a system folder, with your own tool for that (device management, Ansible, an image, a login script). The agent then runs zn-gate's hooks for everyone, and for Claude Code and Codex you can also stop users and projects from switching them off.

zn-gate does not deploy these files for you: it is a local guardrail, not an MDM (see [Team overview](https://usezn.com/docs/team/)). Everything below works by hand today.

Facts about each agent come from that agent's own documentation, read on 2026-10-19; check it again before a large rollout, because these settings change. The hook commands are the ones `zn hooks install` writes for a project install in zn-gate 1.12.0 or later.

## Before you start

1. **Install zn-gate on every machine**, the same version everywhere. It needs Node.js 18 or later: `npm install -g zn-gate`. Do this first. A hook that cannot run is not harmless: Claude Code and Codex let the tool call go ahead unchecked, and Cursor blocks file reads and prompts when a hook has `failClosed` set.
2. **Make sure the agent can find `zn`.** The commands below call `zn hook <agent>`, so `zn` must be on the PATH the agent runs with. Desktop apps do not always get the PATH of your terminal. If you are not sure, write the full path instead (`command -v zn` on macOS and Linux, `where zn` on Windows), for example `/usr/local/bin/zn hook claude-code`.
3. **Try it on one machine**, check it (see "Check a machine" below), then roll it out.

## Claude Code

Claude Code reads its managed settings from a system folder:

| System | Folder |
|---|---|
| macOS | `/Library/Application Support/ClaudeCode/` |
| Linux and WSL | `/etc/claude-code/` |
| Windows | `C:\Program Files\ClaudeCode\` |

The folder holds `managed-settings.json`, an optional `managed-settings.d/` folder of `*.json` files, and `managed-mcp.json`. Claude Code merges `managed-settings.json` first and then the files in `managed-settings.d/` in alphabetical order, and it lists hooks from every file together. That lets you keep zn-gate's hooks in their own file, for example `managed-settings.d/50-zn-gate.json`, next to the policy other teams own.

### Hooks and allowManagedHooksOnly

```json
{
  "allowManagedHooksOnly": true,
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|PowerShell|Monitor|Read|Grep|LSP|Edit|MultiEdit|Write|NotebookEdit|WebFetch",
        "hooks": [{ "type": "command", "command": "zn hook claude-code", "timeout": 15 }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "^(Bash|PowerShell|Monitor|Read|Grep|LSP|WebFetch|ReadMcpResourceTool)$|^mcp__.*",
        "hooks": [{ "type": "command", "command": "zn hook claude-code", "timeout": 15 }]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [{ "type": "command", "command": "zn hook claude-code", "timeout": 15, "onFailure": "block" }]
      }
    ]
  }
}
```

`"onFailure": "block"` makes Claude Code block a prompt when the hook cannot start. It needs Claude Code 2.1.295 or later; older versions ignore the field.

**From zn-gate 1.13.0**, zn-gate also checks what a typed skill or command expands into. Add this entry under `hooks` for it: `"UserPromptExpansion": [{ "hooks": [{ "type": "command", "command": "zn hook claude-code", "timeout": 15, "onFailure": "block" }] }]`.

**What `allowManagedHooksOnly` does.** With `true`, Claude Code runs only the hooks that come from managed settings (plus hooks that the Agent SDK registers and hooks of plugins your managed settings force-enable). It blocks everything else: the hooks in a user's `~/.claude/settings.json`, in a project's `.claude/settings.json` and `.claude/settings.local.json`, in other plugins and in agent frontmatter. It has side effects: the `/goal` command stops working, and a custom status line must come from managed settings. So decide on purpose, and tell your developers.

**The setting protects zn-gate only if zn-gate's hooks are in the managed settings.** If you set `allowManagedHooksOnly` without them, the hooks that a developer installs with `zn hooks install claude-code` or the zn-gate plugin stop running, and Claude Code shows no message. zn-gate tells the developer in `zn hooks install claude-code` and `zn-gate status`, and stays quiet when the managed files hold zn-gate's hooks.

Also keep in mind:

- `disableAllHooks: true` in managed settings switches off every hook, yours included. Leave it unset.
- If you deliver policy in more than one way (server-managed settings, device management, the Windows registry and files), Claude Code by default uses the highest-ranked source that sets a policy and ignores the others, so a file can be ignored. Put the zn-gate hooks in the source Claude Code uses on that machine. `/status` shows it on the `Setting sources` line. zn-gate reads only the files.

### MCP servers (optional)

The hooks are what protect the agent. If you also want developers to use the zn-gate `secrets` MCP server (it lets the agent use a secret kept in zn's vault without seeing it), the managed MCP rules decide whether it can start. Be careful: **a `managed-mcp.json` file takes exclusive control.** Once it exists, Claude Code loads only the servers in that file (and a few built-in ones), and users cannot add any other server. List every server your team needs, not only zn-gate's. With an empty `mcpServers` object, no server loads at all.

`managed-mcp.json` uses the same format as a project `.mcp.json`:

```json
{
  "mcpServers": {
    "secrets": {
      "type": "stdio",
      "command": "zn",
      "args": ["secrets-mcp", "--agent", "claude-code"]
    }
  }
}
```

If you do not want exclusive control, use the allowlists `allowedMcpServers` and `allowManagedMcpServersOnly` in managed settings instead, and add the `secrets` server to the allowlist. See Claude Code's [Managed MCP configuration](https://code.claude.com/docs/en/managed-mcp). `zn-gate status` tells a developer when a managed file or list stops the `secrets` server.

## Cursor

Cursor reads enterprise hooks from a system `hooks.json`:

| System | File |
|---|---|
| macOS | `/Library/Application Support/Cursor/hooks.json` |
| Linux and WSL | `/etc/cursor/hooks.json` |
| Windows | `C:\ProgramData\Cursor\hooks.json` |

```json
{
  "version": 1,
  "hooks": {
    "beforeMCPExecution": [{ "command": "zn hook cursor", "timeout": 30 }],
    "beforeReadFile": [{ "command": "zn hook cursor", "timeout": 30, "failClosed": true }],
    "beforeShellExecution": [{ "command": "zn hook cursor", "timeout": 30 }],
    "beforeSubmitPrompt": [{ "command": "zn hook cursor", "timeout": 30, "failClosed": true }],
    "postToolUse": [{ "command": "zn hook cursor", "timeout": 30 }],
    "preToolUse": [{ "command": "zn hook cursor", "timeout": 30, "matcher": "Grep|Write|Delete" }]
  }
}
```

This is the same set of hooks as `zn hooks install cursor`, with `failClosed` on the two hooks that read files and prompts. With `failClosed: true`, a crash, a timeout or no answer from the hook blocks the action instead of letting it through. That is why zn-gate has to be installed first: on a machine without `zn`, Cursor would block file reads and prompts.

What Cursor does with several sources: it runs the hooks of every `hooks.json` (enterprise, team, project and user), enterprise first in priority, and a `deny` from any of them wins. So a developer's own hooks still run next to yours. Cursor's documentation (read 2026-10-19) describes no setting that makes only enterprise hooks run, so zn-gate cannot be made mandatory in Cursor the way it can in Claude Code and Codex. Cursor's enterprise plan can also distribute hooks from its web dashboard; this page covers the file only.

zn-gate does not cover the Cursor command line (`cursor-agent`).

## Codex CLI

Codex reads admin requirements from a system `requirements.toml`:

| System | File |
|---|---|
| macOS and Linux | `/etc/codex/requirements.toml` |
| Windows | `%ProgramData%\OpenAI\Codex\requirements.toml` |

On macOS, device management can also deliver the same content as `requirements_toml_base64` in the `com.openai.codex` domain. See OpenAI's [Managed configuration](https://developers.openai.com/codex/enterprise/managed-configuration).

```toml
allow_managed_hooks_only = true

[features]
hooks = true

[hooks]
managed_dir = "/opt/example-managed-hooks"
windows_managed_dir = 'C:\ProgramData\ExampleCorp\managed-hooks'

[[hooks.PreToolUse]]
matcher = "^(Bash|apply_patch)$"

[[hooks.PreToolUse.hooks]]
type = "command"
command = "zn hook codex"
command_windows = "zn hook codex"
timeout = 30
statusMessage = "zn: checking for secrets"

[[hooks.PostToolUse]]
matcher = ".*"

[[hooks.PostToolUse.hooks]]
type = "command"
command = "zn hook codex"
command_windows = "zn hook codex"
timeout = 30
statusMessage = "zn: checking for secrets"

[[hooks.UserPromptSubmit]]

[[hooks.UserPromptSubmit.hooks]]
type = "command"
command = "zn hook codex"
command_windows = "zn hook codex"
timeout = 30
statusMessage = "zn: checking for secrets"
```

What the keys do, from Codex's documentation:

- `allow_managed_hooks_only = true` makes Codex skip hooks from users, projects, sessions and plugins, and load only managed hooks.
- `[features] hooks = true` keeps hooks on even for a user who turned them off in their own `config.toml`.
- `managed_dir` and `windows_managed_dir` must be absolute paths of folders that exist, or Codex does not load the managed hooks. zn-gate needs no script in them, so an empty folder that you create is enough.
- Managed hooks count as trusted by policy: unlike hooks a developer installs, they do not need the review in `/hooks`, and the user cannot turn them off there.

If the hook cannot start, Codex lets the call (and the prompt) go ahead, so the same rule applies: install zn-gate first.

## Check a machine

**`zn-gate check` at version 1.12.0 reads each user's own setup, not the managed files.** On a machine where zn-gate is deployed only through managed configuration, it says "zn is not set up for this agent", even when the managed hooks work. Do not treat that line as a failure of your rollout. Use these checks instead.

1. **Ask the agent which settings it uses.**
   - Claude Code: run `/status` and read the `Setting sources` line. It must list the enterprise managed settings (for files, "file"). `claude doctor` lists what Claude Code dropped.
   - Cursor: open the Hooks tab in **Customize**, and the Hooks output channel to see hooks that ran and any error.
   - Codex: run `/hooks`. Managed hooks are marked as managed.
2. **Run `zn-gate status`** on a Claude Code machine. It reports, in one line, a managed setting that stops zn-gate's hooks or its `secrets` server (`disableAllHooks`, `allowManagedHooksOnly` without zn-gate's hooks, `managed-mcp.json` without the server, an MCP allowlist or denylist). No line means it found none in the files; it cannot see policy that comes from server-managed settings, device management or the registry.
3. **Test the hook command itself.** On macOS or Linux, in an empty folder, make a test `.env` file with a fake key and ask the hook to read it, as the agent does:

   ```bash
   printf 'ZN_CHECK_EXAMPLE_KEY=zn-check-fake-value-not-a-real-key\n' > .env
   printf '{"hook_event_name":"PreToolUse","session_id":"test","cwd":"%s","tool_name":"Read","tool_input":{"file_path":"%s/.env"}}' "$PWD" "$PWD" | zn hook claude-code
   ```

   The answer must contain `"permissionDecision":"deny"`. For an ordinary file such as `README.txt`, the answer must not. The same test for Codex is a `Bash` call with `"command":"cat .env"` piped to `zn hook codex`; for Cursor it is `{"hook_event_name":"beforeReadFile","cwd":"…","file_path":"…/.env","content":"x","workspace_roots":["…"]}` piped to `zn hook cursor`, which answers `"permission":"deny"`. This proves that the command you wrote starts and that zn's rules block the file. It does not prove that the agent calls it: steps 1 and 2 do that.
4. **See it end to end.** In a throwaway folder that holds only the fake `.env` above, ask the agent to read it. It must be refused with a message that starts with `zn:`.

## Keep it working

- **Update every machine together.** Install the same zn-gate version everywhere, and change the managed files only to match what that version needs; the changelog says when hook entries change (for example, 1.13.0 adds an entry for Claude Code).
- **Join the organization.** Managed hooks run the checks; the organization policy and audit trail need each machine to join with `zn team join`. See [Join a machine](https://usezn.com/docs/team/join/) and [Organization policy](https://usezn.com/docs/team/policy/).
- **To remove zn-gate,** delete the entries from the managed files; `zn hooks uninstall` only removes what it wrote itself.

## What this page does not cover

OpenCode, and agents zn-gate does not support yet (see [Docs for AI agents](https://usezn.com/docs/agents/)). Windows was not tested for the Cursor and Codex files. Tell us at support@usezn.com if something here does not match what you see.
