# The Immiscible CLI

> One command governs the agent in your project. Sign in through the browser, create the agent and its rule, write .env, install the Claude Code hook and make a live test call. Works for people and for AI coding agents running non-interactively.

Source: https://immiscible.fly.dev/docs/cli

The fastest way to put Immiscible in front of an agent:

```bash
npx immiscible init
```

It signs you in if you are not, finds what your project uses, creates the agent and its rule, adds `IMMISCIBLE_URL` and `IMMISCIBLE_AGENT_KEY` to `.env`, installs the Claude Code hook where there is Claude Code, prints the code for your SDK, and ends with a live test call:

```text
✓ Governed by Immiscible: Invoice agent (Acme)
```

The CLI is the `immiscible` package on npm. It needs Node 22.13 or later and has no dependencies. To have an `immiscible` command without `npx`, install it once with `npm install -g immiscible`.

Or install it with one line, which checks Node, installs the package and shows the welcome card:

```bash
curl -fsSL https://immiscible.fly.dev/install.sh | sh
```

```powershell
irm https://immiscible.fly.dev/install.ps1 | iex
```

`immiscible about` shows the card again: the version, the credits and the links. It is drawn only in a terminal; on a pipe or in CI it prints plain text, and `--json` gives the same facts as data. The card draws the Cardinal squares in dots, with the dot field thickening towards the right; under 98 columns it goes compact, and under 74 it is plain. Set `IMMISCIBLE_CARD=compact` or `IMMISCIBLE_CARD=classic` to choose, and `IMMISCIBLE_NO_MOTION=1` to turn off the animation.

This is the developer CLI. The repository also has an operator CLI, `immiscible-server` (run in a checkout as `npm run admin -- <command>`), which runs a server rather than talking to one: backups, integrity checks, the demo seed. They are different programs, and `immiscible-server init` points you back here.

## See it first, with no account 

```bash
npx immiscible try
```

From 0.2.0. In under a minute, offline: a made-up finance agent asks three times, and the fake Immiscible server the SDKs test against, started on 127.0.0.1 with its own signing key, decides. Looking up an invoice is allowed; paying £1,250 to a supplier it has not paid before is held, and you approve or deny it at the prompt; paying a lookalike of a known supplier is denied. Then `try` saves the receipt, runs `immiscible verify` on it, says what just happened, and ends on `immiscible init`. The fake imitates a rule; it is not the real policy engine. Nothing leaves the machine, and the fake stops when `try` ends. Without a terminal, pass `--yes` to approve the held payment.

## Check a receipt 

```bash
immiscible verify receipt.jwt --keys keys.json
```

From 0.2.0. Checks a signed receipt offline, with the same verifier as the SDK: the Ed25519 signature, the key id, the type and the expiry. It prints what was allowed: the action, the amount and where it went, and whether a person approved it. `--keys` takes a saved copy of the issuer's `/.well-known/immiscible-keys.json` (nothing is fetched) or its URL; without it, the keys come from your server and the receipt must be that server's. The receipt can be a file, the token itself, or `-` for stdin. It needs no account and no agent key, and exits 12 when the receipt is not valid.

## Sign in 

```bash
immiscible login
```

The CLI shows a one-time code and opens your browser at `/app/device`. Sign in if you are not, check the code matches your terminal, pick the workspace and choose **Allow**. The terminal says who you are signed in as. This is the OAuth 2.0 device authorization grant ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)), with PKCE: the code in the browser is useless without the secret the CLI kept.

The token is stored in `~/.config/immiscible/credentials.json` (or under `$XDG_CONFIG_HOME`), readable only by you (mode 0600), one entry per server. It acts for you in the workspace you picked and can only read agents and status and add agents with a rule from your templates: it never approves anything or changes an existing rule. It lasts 90 days. End it with `immiscible logout`, from your sessions in the console, or by signing out everywhere or changing your password; an owner or admin can end any CLI sign-in to the workspace. Your workspace's sign-in rules (address allowlist, single sign-on, two-factor, an administrator ending your sessions) apply to it on every use.

| | |
|---|---|
| `immiscible login --url https://immiscible.your-company.com` | sign in to a server you run yourself (or set `IMMISCIBLE_URL`) |
| `immiscible login --token imc_...` | store a token you already have, after checking the server accepts it |
| `IMMISCIBLE_TOKEN=imc_... immiscible status` | use a token without storing it: what CI does |
| `immiscible whoami` | who, which workspace, which server, and where the token came from |
| `immiscible logout` | revoke this machine's token and forget it |
| `immiscible token create --name ci` | a CI token, shown once (see [below](#non-interactive)) |

The server is chosen in this order: `--url`, `IMMISCIBLE_URL`, `IMMISCIBLE_URL` in the project's `.env`, the server you last signed in to, then `https://immiscible.fly.dev`.

## Govern the agent in a project 

```bash
immiscible init
```

1. **Detects the project**, from files only: `package.json` (`openai`, `@anthropic-ai/sdk`, `ai`, `langchain` and `@langchain/*`, `@openai/agents`), `pyproject.toml`, `requirements*.txt` or `Pipfile` (`openai`, `anthropic`, `langchain`, `openai-agents`), a `.claude/` directory or `CLAUDE.md`, MCP configs (`.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`), and x402 or wallet SDKs.
2. **Asks the minimum**: the agent's name, and what it does, from the same purposes as the console's **Add an agent** (each shown with the rule it gets in your workspace). New agents start at the workspace's starting tier, intern unless an owner has chosen junior, as everywhere.
3. **Creates the agent and its rule.** When the agent acts for you and your workspace has another owner, the rule goes to them: *Sent to another owner to confirm; payments start once they do.* Until then the agent is connected and everything it asks for is refused.
4. **Writes `.env`**: adds `IMMISCIBLE_URL` and `IMMISCIBLE_AGENT_KEY`. A value already there is never replaced without asking; a `.env` that points at another server stops init before anything is made (use `--url` to keep it, or `--force`). In a git repository it adds `.env` to `.gitignore` (creating the file if need be), because `.env` holds the agent key; it shows the line and asks first, and `--no-gitignore` leaves `.gitignore` alone.
5. **Installs the Claude Code hook** in a Claude Code project, after showing the change to `.claude/settings.json` and asking: the hook file goes in `.claude/hooks/immiscible-claude-code-hook.mjs`, and one `PreToolUse` entry is added with the full matcher and `|| exit 2`, so it fails closed. Your other settings and hooks are left as they are.
6. **Prints the code** for the SDK it found.
7. **Makes a live test call** through the gate as the agent and prints `✓ Governed by Immiscible: <agent> (<workspace>)`. While the rule still waits for another owner it prints `! Waiting for another owner to confirm the rule` instead, and exits 10 with `"ok": false`. The test is marked as a test and tidies up after itself: nobody is notified, and a question for a person is cancelled at once, so nobody has to decide it.

Running `init` again changes nothing that is already right: the key in `.env` is checked and reused, `.env` and `.claude/settings.json` are left byte for byte, and the test call reuses its idempotency key while the agent's rules are unchanged. Once a rule changes (another owner confirms it, say), the test is made afresh, so the answer printed is always the current one. If the key in `.env` is no longer accepted, init offers to replace it.

The hook entry it writes:

```json
{
  "matcher": "Bash|Write|Edit|MultiEdit|NotebookEdit|WebFetch|mcp__(?!immiscible__(check_action_status|explain_decision|spend_summary|find_waste|unwatched_keys)$).*",
  "hooks": [
    {
      "type": "command",
      "command": "node --env-file-if-exists=\"$CLAUDE_PROJECT_DIR/.env\" \"$CLAUDE_PROJECT_DIR/.claude/hooks/immiscible-claude-code-hook.mjs\" || exit 2",
      "timeout": 60
    }
  ]
}
```

Immiscible's own read-only MCP tools (`check_action_status`, `explain_decision`, `spend_summary`, `find_waste` and `unwatched_keys` on the `immiscible` server) are left out, so the hook never asks Immiscible about asking Immiscible; its tools that act still go through. A new agent's default rule (General tasks) lets provably read-only calls (`ls`, `git status`, `git diff`, `git log`, reading a file) go ahead without a person; each is still decided and recorded. An intern asks before anything that writes, runs or deletes something. Once the agent is past its intern stage, edits, test runs and builds inside its project go ahead without a person (`localWrites: "allow-after-intern"`); the hook sends the project, `CLAUDE_PROJECT_DIR` or the working directory, and a call that names a path outside it asks (from 0.2.0: an older hook does not say which project, so those calls still ask). At every standing and under every rule, these still ask: a destructive command (`rm -rf`, `git push --force`, `git reset --hard`, `git rebase`, `curl ... | sh`), any `git push`, a deploy, a publish, a package install from the network, anything run with `sudo`, and a change to `.claude/settings.json` or git's hooks; a secrets file leaving the machine is refused. The rule says so on the agent's page; to have a person sign off reads too, replace it with one that sets `readOnly: "ask"` or leaves it out; to have a person approve every edit and test run, replace it with one that sets `localWrites: "ask"`.

Beside the hook, init adds Claude Code deny rules to the same file, shown in the same diff: `Bash(rm -rf:*)`, `Bash(rm -fr:*)`, `Bash(sudo rm:*)`, `Bash(git push --force:*)`, `Bash(git push -f:*)`, `Bash(git reset --hard:*)`, `Bash(git clean -f:*)`, `Read(./.env)` and `Read(./.env.*)`. Claude Code refuses those itself, before any hook runs and without the network. Deny rules you already have are kept, in your order.

The hook reads `IMMISCIBLE_URL` and `IMMISCIBLE_AGENT_KEY` from the project's `.env` (a variable already in the environment wins). More on what it sends and how it decides: [Claude Code and Cursor](https://immiscible.fly.dev/docs/guides/mcp-proxy.md).

| Flag | |
|---|---|
| `--name <name>` | the agent's name |
| `--purpose <purpose>` | `pays_invoices`, `books_travel`, `handles_refunds`, `buys_software`, `answers_customers` or `other` |
| `-y`, `--yes` | accept defaults and confirm changes; required when not at a terminal |
| `--hook`, `--no-hook` | install the Claude Code hook without `.claude/`, or never |
| `--no-test` | skip the live test call |
| `--no-gitignore` | leave `.gitignore` alone |
| `--force` | replace `IMMISCIBLE_URL` in `.env` when it points elsewhere |
| `--dir <path>` | the project (default: the current directory) |

## Govern every project on a machine 

```bash
immiscible install claude-code [--scope user|managed] [--transport command|http] [--key <agent key>] [--dry-run]
```

`init` governs one project; `install claude-code` puts the hook in front of every Claude Code session on the machine. `--scope user` (the default) merges it into `~/.claude/settings.json`; `--scope managed`, run as an administrator, writes Claude Code's managed settings file, which people cannot override, and sets `allowManagedHooksOnly` so only managed hooks run. The default command transport fails closed; `--transport http` has Claude Code post each event to your server instead, and lets a call go on if the server cannot be reached at all. It shows the diff and asks first (`--yes` when there is no terminal), `--dry-run` writes nothing, and a second run changes nothing. After copying the hook it runs `node <hook> --self-test`. Pair it with the **Coding agent baseline** rule. More in [the Claude Code fleet pack](https://immiscible.fly.dev/docs/guides/claude-code-fleet.md).

| Flag | |
|---|---|
| `--scope <scope>` | `user` or `managed` |
| `--transport <transport>` | `command` (fails closed) or `http` |
| `--key <key>` | an agent key to write into the settings' `env`; left out, each person's environment supplies `IMMISCIBLE_AGENT_KEY` |
| `--gateway <url>` | send Claude Code's model traffic through your Immiscible gateway (`ANTHROPIC_BASE_URL`) |
| `--dry-run` | show the change, write nothing |
| `-y`, `--yes` | write without asking; required when not at a terminal |

## Hooks for Codex, Cursor, Windsurf and Gemini CLI 

```bash
immiscible install codex                     # or cursor, windsurf, gemini; for you
sudo immiscible install codex --scope managed  # for everyone on this machine
immiscible install gemini --dry-run          # show the change, write nothing
```

Copies one hook, `coding-agent-hook.mjs` (Node, no dependencies), and adds one entry to the agent's own hook configuration, so the agent asks Immiscible before each shell command and MCP tool call, and before each file write where it has that event. The same rules decide as for the Claude Code hook. It fails closed: when Immiscible cannot answer, the call is refused, and the command ends in `|| exit 2` so a missing file or a missing `node` blocks it too. Every other setting and hook in the file stays as it is, an older Immiscible entry is replaced in place, and running it again changes nothing. After copying the hook it runs `node <hook> --self-test`, and only then changes the agent's configuration.

| Agent | `--scope user` (the default) | `--scope managed` |
|---|---|---|
| Codex | `~/.codex/hooks.json` | `requirements.toml` (`/etc/codex` on macOS and Linux), with `allow_managed_hooks_only = true` |
| Cursor | `~/.cursor/hooks.json` | the enterprise `hooks.json` (`/Library/Application Support/Cursor`, `/etc/cursor`, `C:\ProgramData\Cursor`) |
| Windsurf | `~/.codeium/windsurf/hooks.json` | the system `hooks.json` (`/Library/Application Support/Windsurf`, `/etc/windsurf`, `C:\ProgramData\Windsurf`) |
| Gemini CLI | `~/.gemini/settings.json` | the system `settings.json` (`/Library/Application Support/GeminiCli`, `/etc/gemini-cli`, `C:\ProgramData\gemini-cli`), with `hooksConfig.enabled` |

The hook reads `IMMISCIBLE_URL` and `IMMISCIBLE_AGENT_KEY` from the environment, then from `hook.env` beside it (written with your server's address, 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 how they find the key. When a decision needs a person, Cursor asks you at the keyboard with the approval link; Codex and Gemini CLI wait up to four minutes for someone to approve in Slack, Teams, email or the console; Windsurf documents no hook timeout, so it refuses with the link and you run it again once approved. More in [hooks for other coding agents](https://immiscible.fly.dev/docs/guides/coding-agent-hooks.md).

| Flag | |
|---|---|
| `--scope <user\|managed>` | for you, or for everyone on the machine (run as an administrator) |
| `--key <key>` | an agent key, written to `hook.env` (left out, each person's environment supplies it) |
| `--dry-run` | show the change and write nothing |
| `-y`, `--yes` | write without asking; required when not at a terminal |

With `--json`: `{ ok, target, scope, config, configState, hookFile, envFile, command, diff, changed, selfTest, warnings }`. `install` is in the next CLI release; until it is published, run it from a checkout as `node packages/immiscible-cli/bin/immiscible.mjs install codex`.

## Check the setup 

```bash
immiscible doctor
```

| Check | Fails when |
|---|---|
| Node.js | warns below 22.13 (the hook command needs `--env-file-if-exists`) |
| Server | `/healthz` does not answer |
| Clock | this machine is 60 seconds or more from the server (signed receipts allow 60); warns from 5 |
| Signed in | the token is not accepted (not being signed in only warns) |
| Environment | `IMMISCIBLE_URL` or `IMMISCIBLE_AGENT_KEY` is missing from `.env` and the environment; warns when the shell's value differs from `.env` (the hook uses the shell's, so doctor checks that one) |
| Agent key | the key is not accepted, or the agent is stopped; warns when it has no rule yet |
| Claude Code hook | in a Claude Code project: not installed, not the full matcher, the command does not end in `exit 2`, a timeout above 60, or it does not refuse when Immiscible cannot be reached (doctor runs it against an address nothing answers on); warns that the hook is out of date when the file differs from the one this CLI ships or (from 0.2.0) the one your server serves |
| .gitignore | warns when `.env` holds the key and is not ignored |

Each problem comes with a fix and a link here. The exit code is 0 when nothing failed and 7 when something did.

## What needs you 

```bash
immiscible status
```

What waits for approval (with a link to each), today's decisions since 00:00 UTC (allowed, asked a person, refused; `init`'s own connection tests are not counted), and this month's AI spend measured by the gateway and agent payments, in your workspace's books currency at the newest European Central Bank rate.

## Evidence for an auditor 

```bash
immiscible evidence ai-act --out pack.zip
```

Downloads the [EU AI Act deployer evidence pack](https://immiscible.fly.dev/docs/guides/eu-ai-act-deployers.md) for the workspace you are signed in to, from 0.3.0 of the CLI (not yet published on npm): a zip with `pack.json` and a readable `SUMMARY.md`, or the JSON alone when `--out` ends in `.json`. Owners, admins, security admins and auditors can export it; other roles get exit code 6. It never replaces a file unless you pass `--force`, and it exits 12 when the pack reports that the ledger did not verify, so a scheduled export notices.

## What your agents can touch 

```bash
npx immiscible check            # this project and your home directory; nothing is sent
npx immiscible check --upload   # also a report link from the findings, for seven days
```

The local half of the [AI check](https://immiscible.fly.dev/check). It reads files and runs nothing: the MCP servers in `.mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, `~/.claude.json`, `~/.cursor/mcp.json`, Claude Desktop, Windsurf and Gemini CLI, and what each can do (make payments, run shell commands, write to a database, send email); Claude Code's permission settings (`Bash(*)`, `bypassPermissions`, MCP tools allowed without asking); and provider keys in `.env` files, MCP configs and shell config (`~/.zshrc`, `~/.bashrc`, `~/.profile` and the like), with whether git ignores the file. No sign-in is needed.

```text
  Checked ~/code/hollis-agents (nothing left this machine)

  Agents  OpenAI Agents SDK, LangChain
  MCP     3 servers · stripe can make payments · postgres can write to a database
  Claude  Claude Code may run any shell command without asking (Bash(*) is allowed)
  Keys    2 provider keys in .env · 1 in shell config

  Riskiest first
  1  stripe (MCP, .mcp.json) can make payments with a live secret key, so with no limit but the account's, and nothing asks a person first.
  2  Claude Code may run any shell command without asking (Bash(*) is allowed), in .claude/settings.json.
  3  postgres (MCP, .mcp.json) can write to a database, and nothing asks a person first.
```

A key is never printed or sent. It is shown as its provider, its prefix and last four characters (as the provider's own console shows it) and a fingerprint, `sha256:` and the first 12 hexadecimal characters of its hash, so you can tell two keys apart without seeing either. `--upload` sends only the findings, the sentences above, to `POST /api/check/upload`: no key, redacted or not, no file contents, and no full path (a file outside the project is named from your home directory, such as `~/.zshrc`). It cannot see whether a file is already committed (that needs git itself) or what a key may do at its provider. With `--json`: `{ ok, exitCode, agents, mcp, claude, env, keys, findings, uploaded }`. The exit code is 11 when something is high risk (a payment tool that never asks, any shell command allowed, a key in a file git does not ignore) and 0 otherwise.

## What your agents did 

```bash
npx immiscible scan                        # the last 7 days; nothing is sent
npx immiscible scan --since 30d --html report.html
```

Reads the session history coding agents already keep on your machine, from 0.3.0 of the CLI (not yet published on npm), and needs no account.

| Agent | Read from |
|---|---|
| Claude Code | `~/.claude/projects/*/*.jsonl` (or `$CLAUDE_CONFIG_DIR/projects`), and the hooks and `permissions.defaultMode` in `~/.claude/settings.json` and each project's `.claude/settings.json` and `settings.local.json` |
| Codex | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` (or `$CODEX_HOME/sessions`), and `approval_policy` and `sandbox_mode` in `~/.codex/config.toml` |
| Gemini CLI | `~/.gemini/tmp/*/chats/session-*.json`, and the hooks and approval settings in `~/.gemini/settings.json` |
| Cursor, Windsurf | noted when installed, not read: both keep their history in a database whose format is not documented |

It reports sessions by agent and repository; commands run, flagging force pushes, `rm -rf`, `terraform apply` or `destroy`, `kubectl` and package publishes; files read and written, flagging reads of `.env`, keys, `*.pem` and cloud and registry credentials; outbound calls and the domains reached; a secret read followed by a network call in the same session, the shape of a secret leaving the machine; cost per session from the recorded token counts at list prices (a model without a published price is shown as not priced, never guessed); and the permission modes and hooks in effect, including bypass modes and hooks that are not Immiscible's. It leads with the three most important findings, one sentence each, then the sections. An agent that is not installed is skipped with a note.

It also looks for secrets the agents' own history files hold in plain text (agents write tool output to their transcripts word for word: a `cat .env`, a token in a failed deploy's output): private keys, AWS, GitHub, Slack, npm, Stripe, OpenAI, Anthropic, OpenRouter and Google keys, by their published shapes. Each distinct secret is counted once, reported by kind and folder only, and the finding says to rotate them. If your git email is at a company domain, it counts the other people at that domain who committed to these repositories in the last 90 days, from git on your machine, and shows the number; `--no-team` turns that off.

No prompt text, file contents or secret values are printed or written, in any format: only paths, command names, domains and counts, and the output says so at the top. A command is reduced to the programs it runs, the secret paths it reads and the hosts it reaches; the command line itself is never shown. `--html <file>` writes the same report as one self-contained page with no script and no remote resource. With `--json`: `{ ok, exitCode, local, privacy, window, sources, totals, byAgent, byRepo, commands, files, network, mcp, exfiltration, permissions, sessions, transcripts, team, findings }`. The exit code is 11 when something is high risk (a secret read then the network, a live secret in the history files, a force push, approvals bypassed, a `SessionStart` hook that is not Immiscible's) and 0 otherwise.

## Fix it in one command 

```bash
npx immiscible guard              # every coding agent on this machine, local rules, no account
npx immiscible guard --dry-run    # every change, nothing written
npx immiscible guard --off        # put every file back exactly as it was
```

The other half of `scan`, from 0.3.0 of the CLI (not yet published on npm): guard puts one fail-closed hook in front of each coding agent it finds (Claude Code, Codex, Cursor, Windsurf, Gemini CLI), with the same hooks `install` writes. With no account the hooks decide on your machine, sending nothing anywhere, from a short set of rules:

| | |
|---|---|
| Refused | force pushes to `main`, `master`, `release/*` or production; `rm -r` of the root or home directory; a secret file and a network or upload command in one call; disk wipes; network or shutdown lines added to shell start-up files |
| Asked | publishing a package or image; `terraform`, `pulumi`, `kubectl` and `helm` changes; `curl \| sh`; `sudo`; history rewrites; dropping a database; edits to agent configuration (`.claude/settings.json`, `hooks.json`, `.mcp.json`), shell start-up files and CI workflows; and any network call after the session read a secret file |
| Allowed | everything else |

Claude Code and Cursor ask you at the keyboard. Codex, Windsurf and Gemini CLI cannot ask from a hook, so a call that needs a person is refused with the way to go ahead (run it yourself if you meant it). Before a call that deletes or overwrites files in a git repository, the hooks take a checkpoint, so [`undo`](#undo) can put the files back.

For Claude Code, guard also keeps the settings honest during a session, from 0.3.0 of the CLI. At the start of a session the hook notes the hooks, broad permission rules, bypass mode and MCP switches in the user, project and local settings. Claude Code reloads a settings file that changes mid-session and runs the [`ConfigChange` hook](https://code.claude.com/docs/en/hooks#configchange) first; a change that adds a hook, removes Immiscible's, turns hooks off, allows `Bash(*)` and the like, turns on bypass permissions or adds an MCP server is kept out of the session, with the reason and "if you made the change, restart Claude Code to load it". That is how a planted hook, such as the one the Shai-Hulud worm wrote into `.claude/settings.json`, or a script the agent ran gets in. Managed settings cannot be blocked and are not judged. A project hook new to the machine is named once when a session starts. Hook commands are kept only as a hash and the program's name, never their arguments. A session's "read a secret" mark is kept in `~/.immiscible/state` as the file's short name, never its contents, for a day.

`--connect --key <agent key>` points the same hooks at your Immiscible server instead, so a named person approves in Slack, Teams, email or on the phone and every decision is signed. `--off` puts back every file guard changed, from `~/.immiscible/guard.json`: a file it created is removed, a file it changed is restored byte for byte, and a file someone has changed since guard wrote it is left alone and named. `--agents claude-code,codex` limits it to some agents; `--scope managed` writes each agent's managed settings, for everyone on the machine. With `--json`: `{ ok, mode, scope, dryRun, state, agents, refused, asked, changed, selfTest }`.

## Undo what an agent deleted 

```bash
npx immiscible undo                       # the checkpoints of the last 7 days
npx immiscible undo 8b38f79ca2 --dry-run  # what would change
npx immiscible undo 8b38f79ca2            # put the files back
```

Undo before approve, from 0.3.0 of the CLI (not yet published on npm). Before a call that deletes or overwrites files in a git repository (`rm`, `git clean`, `git reset --hard`, `git checkout --`, `git restore`, `find -delete`, edits to agent configuration), the guard's hooks take a checkpoint: every tracked and untracked file git does not ignore, kept as a commit under `refs/immiscible/checkpoints/` in that repository. It takes about a tenth of a second in a repository of a thousand files. Nothing is pushed, because git pushes branches and tags and never this namespace, and checkpoints older than 7 days are deleted as new ones are taken. `IMMISCIBLE_CHECKPOINTS=off` turns them off.

When Claude Code or Cursor asks you to approve such a call, the question ends "If you approve, it can be undone: npx immiscible undo <id>". A question about something that reaches past the machine, such as a publish, a deploy or the network, ends "This one cannot be undone from here." Codex, Windsurf and Gemini CLI refuse rather than ask, so a refused call takes no checkpoint.

`undo <id>` shows what will change and asks, then takes a checkpoint of how things are now, so the undo can itself be undone. It writes the files back byte for byte with their modes, removes files made since, and restores the index, so staged work stays staged. Commits, branches, ignored files and anything outside the repository are left as they are. Pass `--yes` when there is no terminal. With `--json`: `{ ok, id, repo, command, changes, headMoved, restored, undoWith }`.

## Replay a session 

```bash
npx immiscible replay                          # the sessions of the last 7 days
npx immiscible replay 3f2a9c1d                 # one session in full, by the start of its id
npx immiscible replay 3f2a9c1d --html run.html # the same, as one self-contained page
```

The flight recorder for your coding agents, from 0.3.0 of the CLI (not yet published on npm). It joins two records already on your machine, the session history Claude Code, Codex and Gemini CLI keep and the decision log `guard`'s hooks write, into one timeline per session: every command, file read and write and fetch in order, with the decision the guard made on each and the rule behind it. A call the guard refused is marked as not run, and a refused call the agent never got to make still appears, on its own line.

The hooks write one file a day to `~/.immiscible/decisions` (`IMMISCIBLE_LOG_DIR` to move it, `IMMISCIBLE_LOCAL_LOG=off` to stop it). Each line carries the SHA-256 hash of the line before, so replay notices a line removed or edited, names the file and line, and exits 12. Commands are shown with credentials redacted, by their known shapes and by how random they look; prompt text and file contents are never shown. Nothing leaves the machine and no account is needed. With `--json`, `{ ok, local, chain, sessions }` for the list and `{ ok, local, chain, session }` for one.

## Add the MCP server 

```bash
immiscible mcp                       # every client
immiscible mcp --client claude-code  # or cursor, vscode, windsurf, codex, gemini
```

Prints the one-line command or the config entry that adds Immiscible's MCP server to each client, for the server you use, and changes nothing. The configs read the agent key from `IMMISCIBLE_AGENT_KEY` (which `init` writes to `.env`) or sign in with OAuth; the key itself is never printed. For Cursor it also prints a one-click install link. With `--json`: `{ ok, url, keyVariable, clients: { <id>: { title, command, file, config, note } } }`. More in [add the MCP server](https://immiscible.fly.dev/docs/answers/add-the-mcp-server.md).

## In CI and AI coding agents 

Every command works without a terminal. Nothing prompts: input comes from flags, and a command that would need to ask stops with exit code 4 and says which flag to pass. Colour is off when stdout is not a terminal or `NO_COLOR` is set, and there is no spinner.

```bash
export IMMISCIBLE_URL=https://immiscible.fly.dev    # or your own server
export IMMISCIBLE_TOKEN=imc_...            # from token create --name ci, shown once
immiscible init --yes --name "Release agent" --purpose other --json
immiscible doctor --json
```

With `--json`, stdout carries one JSON object and nothing else: `ok`, `exitCode`, and the command's result (for `init`: the agent, the rule or the proposal waiting for a second owner, what changed in `.env`, the hook and its diff, the snippet, and the test call; for `doctor`: every check with its status, fix and docs link). Errors are `{ "ok": false, "exitCode": 3, "error": { "code", "message", "fix" } }`. `immiscible help --json` describes every command, flag and exit code as data. `login --json` prints two lines: the code to show a person first, then the result.

For CI, make a token for the job rather than reusing your own sign-in: `immiscible token create --name ci` prints a CI token once. It acts for you in the workspace you are signed in to, with your CLI scopes or fewer (`--read-only` leaves out adding agents), and expires in 90 days or sooner (`--days`). `immiscible token list` and `immiscible token revoke <id>` manage them; owners and admins can also make one in the console, beside service tokens, and see and revoke every CLI sign-in there.

## Exit codes 

| Code | Meaning |
|---|---|
| 0 | done |
| 1 | unexpected error (a server error, a file that could not be written) |
| 2 | usage: an unknown command or flag, or a bad value |
| 3 | not signed in, or the token is no longer accepted: run `immiscible login` |
| 4 | input needed and this is not a terminal: pass the flags it names |
| 5 | the server could not be reached |
| 6 | the server refused: a role, a rule, a plan limit |
| 7 | `doctor`: a check failed |
| 8 | `login`: denied in the browser, or the code expired |
| 9 | `init`: the test call did not come back governed |
| 10 | `init`: done, and the rule waits for another owner to confirm |
| 11 | `check` or `scan`: something high risk was found |
| 12 | `verify`: the receipt is not valid (altered, expired, an unknown key or another issuer); `evidence`: the ledger did not verify; `replay`: the decision log's hash chain is broken |

The API the CLI uses is in the reference under [Developer CLI](https://immiscible.fly.dev/docs/api/endpoints.md).
