# The Claude Code fleet pack

> Govern every Claude Code session on a machine, or on every machine you manage, with one command. A managed hook people cannot switch off, a baseline rule that refuses force pushes to main and asks before terraform apply in production, and an http hook endpoint.

Source: https://immiscible.fly.dev/docs/guides/claude-code-fleet

`immiscible init` governs one project. The fleet pack governs a whole machine: every project a person opens, or, through Claude Code's managed settings, every person on the machine, with a hook they cannot remove.

```bash
# For yourself: every project you open
npx immiscible install claude-code --key "$IMMISCIBLE_AGENT_KEY"

# For everyone on this machine (as an administrator, or from your device management tool)
sudo npx immiscible install claude-code --scope managed --key ask_...
```

It shows the change to the settings file first, and writes only on a yes (`--yes` when there is no terminal). `--dry-run` shows the change and writes nothing. Running it again changes nothing, byte for byte.

## What it writes 

| | `--scope user` (the default) | `--scope managed` |
|---|---|---|
| Settings | `~/.claude/settings.json` | Claude Code's managed settings file: `/Library/Application Support/ClaudeCode/managed-settings.json` on macOS, `/etc/claude-code/managed-settings.json` on Linux and WSL, `C:\Program Files\ClaudeCode\managed-settings.json` on Windows ([Claude Code settings](https://code.claude.com/docs/en/settings)) |
| Hook file | `~/.immiscible/claude-code-hook.mjs` | `immiscible/claude-code-hook.mjs` beside the managed settings |
| Other hooks | kept | kept in the file, and `allowManagedHooksOnly: true`, so hooks from people's own settings, projects and plugins do not run |

In both, it adds one `PreToolUse` entry with the same matcher `init` uses, `IMMISCIBLE_URL` in the settings' `env` (and `IMMISCIBLE_AGENT_KEY` with `--key`, `ANTHROPIC_BASE_URL` with `--gateway`), and the same Claude Code deny rules as `init` (`Bash(git push --force:*)`, `Read(./.env)` and the rest), after any you already have. Every other setting and hook is left as it is; an older Immiscible entry is replaced in place. After copying the hook it runs `node <hook> --self-test`, which builds a sample request and sends nothing, so a machine whose Node cannot run the hook is found at install time.

Without `--key`, each person's environment must set `IMMISCIBLE_AGENT_KEY`; until it does, the hook refuses every call it checks. A key written into managed settings can be read by everyone on the machine: use an agent made for the fleet, whose rule is the baseline below, not one that can pay or share data. Claude Code reads hooks when a session starts, so sessions already open keep their old hooks until restarted.

## Two transports 

`--transport command` (the default) runs the hook with node. It fails closed: when Immiscible cannot be reached, answers with an error or does not answer in time, the call is refused, and the command ends in `|| exit 2` so a missing file or a missing node blocks the call too.

`--transport http` has Claude Code send each `PreToolUse` and `PermissionRequest` event straight to your server, at [`POST /v1/hooks/claude-code`](https://immiscible.fly.dev/docs/api/post-v1-hooks-claude-code.md), with the agent key in the `Authorization` header (read from `IMMISCIBLE_AGENT_KEY` through the hook's `allowedEnvVars`). The server answers every problem it can see, a missing or unknown key, an event it cannot read, an error, with a refusal. But when the server cannot be reached at all, Claude Code treats the failed http hook as no answer and the call goes on. Choose http where an outage must not stop work, and command where nothing may run unchecked. In managed scope, an organisation that already keeps `allowedHttpHookUrls` or `httpHookAllowedEnvVars` has the endpoint and its two variables added to those lists; a list it does not keep is not started.

The http hook cannot read the session transcript on the person's machine, as the command hook does, so it does not say whether web pages or MCP output entered the session. The gate treats provenance it was not told as untrusted, the safe side; when the session's model traffic goes through the [gateway](https://immiscible.fly.dev/docs/guides/gateway.md), the gateway still says what it saw.

For `PermissionRequest` (Claude Code is about to show its own permission dialog), the endpoint only ever refuses. When Immiscible would allow or ask, the dialog is left to the person: Immiscible never grants more than Claude Code's own settings do.

## The baseline rule 

Give the fleet's agent the **Coding agent baseline** rule (template `coding_agent_baseline`). It is the Coding agent rule (read-only calls go ahead; once past its intern stage, edits and test runs inside the project go ahead; the network only to `github.com` and `registry.npmjs.org`) with command rules added:

| | Command | What the decision says |
|---|---|---|
| Refused | a force push to `main`, `master` or `release/*` (`--force`, `--force-with-lease`, `-f` or `+branch`) | Coding agent baseline refuses a force push to main, master or a release branch, which rewrites history others share. |
| Refused | `rm -rf /`, `rm -rf ~`, `rm -rf $HOME` | Coding agent baseline refuses rm -rf of the root or home directory. |
| Refused | a secrets file (`.env`, a `.pem` key, an SSH key) read and piped to the network, or uploaded with `curl` | Coding agent baseline refuses reading a secrets file (.env, a .pem key or an SSH key) and sending it over the network. |
| Asks | `terraform apply` or `destroy` (and `tofu`) | A person must approve terraform apply or destroy, which changes real infrastructure. |
| Asks | `kubectl` or `helm` with a context whose name contains `prod` | A person must approve kubectl or helm against a production context. |

Everything else is decided as under Coding agent, and the checks that hold under every rule still apply: any `git push`, a deploy, a publish or an install asks a person, a destructive command asks, and a secrets file never leaves the machine. So `git push origin feature/x` is not refused by the baseline; it still asks a person, as every push does.

### Command rules are data 

The command rules are part of the rule, signed with it, not code. Any action rule can carry them:

```json
{
  "kind": "action",
  "title": "Coding agent baseline",
  "actions": ["tool.call"],
  "domains": ["github.com", "registry.npmjs.org"],
  "commands": {
    "deny": [{ "match": "\\bgit\\s[^;&|]*\\bpush\\b...", "reason": "a force push to main, master or a release branch, which rewrites history others share" }],
    "approve": [{ "match": "\\bkubectl\\s[^;&|]*--context(?:=|\\s+)\\S*\\bprod", "reason": "kubectl against a production context" }]
  }
}
```

`match` is a regular expression, read without regard to case against the Bash command as sent and as Immiscible reads it (quotes and backslash escapes taken out), so either spelling matches. `reason` names what the command does, and reads in the decision as "<rule> refuses <reason>." or "A person must approve <reason>.". Up to 50 of each; a pattern that repeats a group with a repeat inside it, such as `(a+)+`, is refused because it can take too long to check. To name your own production contexts, copy the template's rule and change `prod` in the pattern. Removing a command rule from a live rule widens what the agent may do, so it goes to another owner like any other widening.

## Immiscible's own MCP tools 

The two Immiscible MCP tools that change something, `set_budget` and `revoke_key`, carry `_meta: { "anthropic/requiresUserInteraction": true }`, so Claude Code asks the person before calling them, even in a mode that would let it go ahead; a person then approves the change itself in Slack or the console. This is built to the published Claude Code behaviour and is not tested against a live Claude Code here.

## Not in this pack yet 

A per-session view (tokens, cost, actions, denials and the pull request a session ended in) is not built; it is planned with the cross-vendor flight recorder. Installers for Codex, Cursor, Windsurf and Gemini CLI are planned too. Until then, govern those through [MCP](https://immiscible.fly.dev/docs/guides/mcp-proxy.md).
