Install nd7 on macOS
Installing nd7 is one cargo install. It builds two programs, nd7 and nd7-exec, and puts them side by side in ~/.cargo/bin. This page covers what you need first, the command, why the two binaries must stay together and be writable only by you, the developer setup, how to check the install worked, and what to delete if you want it gone.
What you need#
macOS, and a Rust toolchain. If you do not have one, install it from rustup.rs. Nothing else: nd7 needs no privileges, no kernel extension, no entitlement and no background service.
The sandbox is macOS only. On any other platform nd7 run and nd7-exec print only supported on macOS and exit 2; the rest of the tool (recording, verifying, shipping) builds and runs elsewhere, and continuous integration covers Linux as well as macOS.
Install#
cargo install --locked --git https://github.com/nd7-dev/nd7-core
That builds and installs both binaries into ~/.cargo/bin. From a local checkout of the repository, the same thing:
cargo install --locked --path .
--locked means cargo uses the dependency versions recorded in the repository's Cargo.lock rather than resolving newer ones.
Why there are two binaries, and why they must sit together#
nd7 is the command line: it records hook events, checks logs, ships them, registers its hooks with the agents, and starts a sandboxed session. nd7-exec is a small, separate program that is the only thing the sandbox lets out of itself; every Bash command the agent runs goes through it, and it applies the session's current rules to itself before running anything. How the macOS sandbox works explains that mechanism.
Two consequences for the install:
- They must stay next to each other.
nd7 runlocatesnd7-execby resolving its own path and looking beside it, never by searchingPATHor reading an environment variable. In the source that isexit_path()in src/bin/nd7.rs, which takes the current executable, canonicalises it, and replaces the file name withnd7-exec. - They must not be writable by anyone but you. Before starting a session,
nd7 runchecks thatnd7-execexists, that it is a regular file, and that neither it nor the directory holding it is writable by group or others. If either is, it refuses to start with a message naming the path and its mode, because a binary someone else can replace is a binary that can be made to run outside the sandbox.
A normal cargo install into ~/.cargo/bin satisfies both. If you copy the binaries somewhere else, copy both, into a directory only you can write.
Developer setup#
While working on nd7 itself, symlink the installed names to the debug build instead of reinstalling after every change:
ln -sf "$PWD/target/debug/nd7" ~/.cargo/bin/nd7
ln -sf "$PWD/target/debug/nd7-exec" ~/.cargo/bin/nd7-exec
The agent spawns hooks fresh for every event, so the next hook picks up each cargo build without restarting Claude Code. One caveat from the benchmarks: a debug build is roughly 1 ms slower per hook run than a release build, and the published numbers are all for release builds.
Check the install#
nd7 --help
prints the list of commands: record, verify, enroll, ship, run, init, hook-prefix, allow and deny. Running nd7 with no arguments prints the same usage and exits 2, and an unknown command exits 2 as well.
nd7-exec
prints its own usage and exits 2. That is the expected result outside a session: nd7-exec runs a command only when it can find the session it was started in and that session's rules, and refuses with exit 126 otherwise.
To check the sandbox end to end, run any program under it:
cd your-project
nd7 run zsh
nd7 run prints one line naming the session before the program starts. Inside that shell, a write into the project succeeds and a write into your home directory outside the project does not. A session, start to finish walks through the same thing with claude.
What nd7 writes to disk#
Two directories under your home, and one file beside them:
$XDG_STATE_HOME/nd7/(default~/.local/state/nd7/): holdssessions/<session-id>/with the event log, its chain head and its lock;vault/with the machine key and pinned admin keys. When: recording, and enrolment with a vault.~/.nd7/sessions/<pid>/: holdsrecord.jsonandpolicy.sb, the rules of onend7 run. When: while a sandboxed session is running.~/.nd7/aliases.sh:alias claude='nd7 run claude'andalias codex='nd7 run codex', plus one line in~/.zshrc, and in~/.bashrcif you have one, that loads it. When:nd7 initwithout--no-alias. The aliases live under~/.nd7because that is the one directory no session may write to, so a session cannot take away the aliases that put the next session under the sandbox.
nd7 init also writes hooks into the agents' own configuration, ~/.claude/settings.json and ~/.codex/config.toml; see Set up nd7's hooks.
The session directory is short-lived: nd7 run removes it on exit, and the next nd7 run sweeps up any left behind by a run that was killed outright. It is also the one place nd7's own rules can never make writable, from inside or out.
Uninstall#
The repository does not document an uninstall command, and there is none. To remove nd7 by hand:
cargo uninstall nd7-core # removes nd7 and nd7-exec from ~/.cargo/bin
rm -rf ~/.local/state/nd7 # session logs, and the vault keys if enrolled
rm -rf ~/.nd7 # sandbox session records, and the aliases
Adjust the second path if you set XDG_STATE_HOME. If you ran nd7 init, also remove what it wrote into the agents' own configuration: the hooks entries in ~/.claude/settings.json or a project's .claude/settings.json, the [[hooks.…]] tables and the [hooks.state] entries marked # nd7: in ~/.codex/config.toml, and the line loading ~/.nd7/aliases.sh from ~/.zshrc and ~/.bashrc; see Set up nd7's hooks. Deleting ~/.local/state/nd7 deletes your recorded sessions, including any not yet shipped to a vault.