Change the rules while the agent works: nd7 allow and nd7 deny
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.
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:
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
WriteandEdittools. They run inside theclaudeprocess, 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 examplend7 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 commandallow, already granted:session <pid>: <path> was already alloweddeny, grant removed:session <pid>: writes under <path> denied from the next commanddeny, 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:
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:
nd7 allow --session 41287 /Volumes/scratch
Try to grant nd7's own records:
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: reading a denial, and the cases a grant cannot fix.
- How the macOS sandbox works: the full rule set and why the boundary holds.
- A session, start to finish:
nd7 run claudefrom the first command to the first denial and back.