---
title: "Change the rules while the agent works: nd7 allow and nd7 deny"
description: What a grant is, how nd7 finds the session, when a change takes effect, and what each command prints.
order: 50
section: Sandbox
---

A session starts with one writable place: the project directory (plus the temp directory, `~/.claude` and Claude Code's scratch). When the agent needs to write somewhere else, you do not have to restart it. `nd7 allow <path>` adds that path to the running session's rules, and the next Bash command already runs under them. This page covers what a grant is, how the right session is found, when the change lands, what `nd7 deny` does, and the exact messages both commands print. Everything here is the behaviour of `grant()` in [src/bin/nd7.rs](https://github.com/nd7-dev/nd7-core/blob/main/src/bin/nd7.rs).

## What a grant is

A grant is one extra **write root**: a directory, and everything under it, that the session may write to. It is nothing else. Reading is already open everywhere except `~/.ssh`, `~/.aws` and `~/.nd7`, so there is nothing to grant there; and there is no grant for network access, for a port, or for a host.

Grants live in the session's record and are rendered into the per-command profile as one line each:

```
(allow file-write* (subpath "/Users/you/data"))
```

They appear only in the profile `nd7-exec` applies to a single command, never in the floor applied to the `claude` process tree. Grants belong to one session and are gone when it ends, along with the session directory.

The path is resolved to its full, symlink-free form before it is stored, because Seatbelt's `subpath` rule matches resolved paths. `nd7 allow ~/data` and `nd7 allow ./data` from the right place record the same grant.

## How the session is identified

A running `nd7 run` owns a directory named after its own process id: `~/.nd7/sessions/<pid>/`. `nd7 allow` lists the numeric entries under `~/.nd7/sessions` and:

- exactly one session running: it is used, no flag needed;
- none: the command fails with `no running nd7 session`;
- several: the command fails with `several sessions are running ([…]); pass --session <pid>`, listing the pids.

The pid is the number `nd7 run` prints when it starts:

```
nd7 run: session 41287 under nd7's policy; a sandbox the program applies itself is refused
```

So with two sessions open:

```sh
nd7 allow --session 41287 ~/data
```

## When it takes effect

At the **next Bash command**. Not the current one, and not immediately inside the `claude` process.

The reason is in the mechanism. `nd7 allow` rewrites the session's `record.json` and `policy.sb`, both through a temporary file and a rename so nothing ever reads a half-written profile. Every Bash command Claude Code runs goes through `nd7-exec`, which reads `policy.sb` fresh and applies it to itself before running the command. So the next command picks up the change. There is no daemon, no socket and no signal.

Two things a grant does **not** reach:

- **The `Write` and `Edit` tools.** They run inside the `claude` process, which is confined by the floor applied when the session started, and the kernel refuses to replace a profile in a running process. Widening those means restarting the session under a wider project, for example `nd7 run claude --resume <id>`.
- **Anything already running.** A long-running command started before the grant keeps the profile it was started with.

## nd7 deny

`nd7 deny <path>` removes a grant. The next Bash command runs without it; commands already running keep what they had.

One deliberate difference from `allow`: `deny` accepts a path that can no longer be resolved. If the directory has been deleted or renamed since it was granted, `allow` would fail because the path no longer resolves, but `deny` falls back to the path exactly as you typed it, so a grant recorded for a directory that no longer exists can still be taken back. In practice that means such a `deny` has to name the path the way the record holds it — the resolved form that `allow` printed when the grant was made.

## What can never be granted

Any path under `~/.nd7`, nd7's own session records. `nd7 allow` refuses it before writing anything:

```
nd7's own records can never be made writable
```

Even if the check were bypassed, the kernel would still refuse: both profiles end with a deny of every write operation under `~/.nd7`, placed last so it beats everything above it. There is a test in `policy.rs` that grants write access to the entire home directory and then confirms that a write under `~/.nd7` is still denied while a write elsewhere in the home directory succeeds.

## Messages and exit codes

Both commands print one line on success and exit 0.

- **`allow`, new grant**: `session <pid>: writes under <path> allowed from the next command`
- **`allow`, already granted**: `session <pid>: <path> was already allowed`
- **`deny`, grant removed**: `session <pid>: writes under <path> denied from the next command`
- **`deny`, was not a grant**: `session <pid>: <path> was not a grant`

Anything that goes wrong — no path given, an unexpected argument, `--session` without a pid, no running session, several running sessions, a path that cannot be resolved, or a path under `~/.nd7` — prints `nd7 allow: <reason>` (or `nd7 deny:`) on standard error followed by the usage line, and exits 2:

```
usage: nd7 allow [--session <pid>] <path>
```

Note that "already allowed" and "was not a grant" are successes, not errors: the session ends up in the state you asked for either way, and nothing is rewritten.

## Examples

Let the agent write a scratch directory outside the project, then take it back:

```sh
nd7 allow ~/data
# session 41287: writes under /Users/you/data allowed from the next command

nd7 deny ~/data
# session 41287: writes under /Users/you/data denied from the next command
```

Pick one of two running sessions:

```sh
nd7 allow --session 41287 /Volumes/scratch
```

Try to grant nd7's own records:

```sh
nd7 allow ~/.nd7
# nd7 allow: nd7's own records can never be made writable
# usage: nd7 allow [--session <pid>] <path>
```

## Related

- [What happens when a command is denied](/docs/denials): reading a denial, and the cases a grant cannot fix.
- [How the macOS sandbox works](/docs/sandbox): the full rule set and why the boundary holds.
- [A session, start to finish](/docs/session-walkthrough): `nd7 run claude` from the first command to the first denial and back.
