Guides
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.
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.
# 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) |
| 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, 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, 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:
{
"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.