# Write guards
> How ivar enforces read-only boundaries across repos — the filesystem layer, the tool guard decision, and per-provider wiring.
Source: https://ivar.run/docs/guide/guards

import { Callout } from 'fumadocs-ui/components/callout';

The write guard is not one mechanism. It operates as two independent layers covering different attack surfaces: the kernel enforces permissions on disk, while a pre-tool hook checks structured write operations before the provider executes them.

<Callout type="warn" title="chmod, not a prompt">
  Prompt instructions asking an agent not to edit read-only code fail under pressure. ivar strips write bits and intercepts write tools before execution — permissions do not depend on the model's cooperation.
</Callout>

## Layer 1 — The filesystem

Repos that are not promoted have their write permissions stripped on disk: `mode & ~0o222`, applied recursively. The kernel refuses any write attempt directly.

When ivar itself needs to write into a read-only worktree — such as pulling upstream changes during `ivar repo pull` or executing a repo setup script — it uses an internal `LiftedGuard` scope. This temporarily restores write permissions and re-applies the stripped mode on drop, ensuring no failure path leaves a worktree writable.

## Layer 2 — The tool guard

The second layer is a shared pre-tool decision executed as:

```sh
ivar guard --provider <provider>
```

Every provider hook delegates to this command, ensuring [Claude Code](/docs/providers/claude-code), [OpenCode](/docs/providers/opencode), and [OMP](/docs/providers/omp) share an identical decision engine.

### The writable set

For a **feature session**, the writable set contains:

- The session view directory (`$IVAR_SESSION_PATH`)
- The session's scratch directory (`.tmp/` inside the view directory), for temporary and working files
- The feature directory (`.ivar/features/<feature>`)
- The worktrees of all promoted repos
- The hall root, except `.ivar/` (docs, `HALL.md`, harness config)
- The committed hall sources under `.ivar/`: `.ivar/skills/`, `.ivar/skills-local/`, `.ivar/setups/`

Every path is canonicalised to prevent symlink traversal escapes. Non-existent target paths are resolved leniently through their existing parent directories so creating new files succeeds.

For a **discovery session**, the writable set contains the session view directory, its scratch directory (`.tmp/` inside the view directory), and the same hall-root and hall-source entries as a feature session. It holds no repos and no feature directory, but it is never absent: an absent set would imply the guard has nothing to enforce, which would leave read-only worktrees mounted under the view directory unprotected.

Under the kernel sandbox, the hall root is granted entry by entry — every top-level file and directory except `.ivar/` that exists when the provider launches. A brand-new top-level file needs a write from outside the sandbox. A directory that holds a protected path — \`.git/\`, \`.claude/\`, \`.opencode/\`, or \`.omp/\` — is itself expanded entry by entry, so a new file directly inside it is still kernel-denied, while a new file inside any other directory, such as \`docs/\`, is not. The hall's git hooks and config, the provider hook and MCP configuration (`.mcp.json`, `mcp.json`, `opencode.json`, `.opencode/node_modules`), and the hall `.env` stay protected. Because the kernel cannot grant `.git/` without also granting `.git/hooks/`, a sandboxed session can edit hall files but cannot `git commit` them in the hall — commit from outside the sandbox.

### The decision

The guard inspects incoming tool calls and checks only **structured write tools**. All other tools are allowed through.

The write tool list is explicit and closed, matched against normalised names so casing and separators (`NotebookEdit`, `notebook_edit`, `notebook-edit`) match uniformly:

- `write`
- `edit`
- `multiedit`
- `notebookedit`
- `applypatch`
- `patch`

If a structured write targets a path inside the writable set, it is allowed. If it targets an unpromoted repo or an unlisted path, it is denied. Denials list the active writable set so the agent can see where writes are permitted. If no ivar session can be resolved from the current working directory or the target path, the guard denies the call with `no ivar session resolves from the cwd or the target path` and names the scratch directory of each live session — one session is named outright, several are listed, and a hall with no live session is told so rather than offered a path.

Shell execution is explicitly not classified or blocked as a structured write tool here — raw filesystem access is constrained by the Layer 1 kernel permissions.

## Per-provider wiring

Each provider integrates with `ivar guard --provider <provider>` using its native extension hook protocol:

| Provider | Integration point | Wire protocol & verdict |
| --- | --- | --- |
| **Claude Code** | `PreToolUse` hook in `.claude/settings.json` | Returns JSON with `hookSpecificOutput` (`permissionDecision`: `allow` or `deny`, `permissionDecisionReason`). Always exits with status `0` because Claude evaluates the JSON body. |
| **OpenCode** | Plugin at `.opencode/plugins/ivar.js` (`tool.execute.before`) | Emits the denial reason on stdout and exits with non-zero status, causing the plugin to throw and block tool execution. |
| **OMP** | Hook module at `.omp/hooks/pre/ivar.js` (`tool_call`) | Hook executes `ivar guard` via `execFileSync`. Denials emit the reason on stdout and exit non-zero so `execFileSync` throws; the hook catches it and returns `{ block: true, reason }`. (An exit status of `0` would bypass blocking). |

### Input shapes

Each provider supplies its own payload structure, which ivar normalises into a unified tool name and target path:

- **Claude Code** sends `tool_name` and `tool_input.file_path`.
- **OpenCode** sends `tool`, `args.filePath`, and `cwd`.
- **OMP** translates internal `toolName` and `input` properties JS-side before invocation, passing `{ tool, args, cwd }` in OpenCode wire format.

ivar accepts `filePath`, `file_path`, or `path` inside `args`.

## Lifting the guard

To make a repo writable within a feature session, promote it:

```sh
ivar feature promote <feature> <repo>
```

Running `ivar session connect` re-materialises the view directory and repairs any permissions or symlinks that drifted.
