Comprehensive manual verification procedures for all nono subsystems across macOS and Linux.
Prerequisites: A built nono binary (make build).
Important: /tmp is in the base system groups with read+write access. All "outside" test paths must use locations NOT in any system group. $HOME/nono-qa/ works because $HOME itself is not in any allow group (only specific subdirs like ~/.claude, ~/.cargo, etc.).
| Feature | Linux | macOS |
|---|---|---|
| Sandbox backend | Landlock | Seatbelt |
| Deny semantics | Allow-list only; no explicit deny | Kernel-enforced (deny ...) rules |
deny.access |
Path simply not in allow list | Blocks content read+write (metadata allowed) |
deny.unlink |
No separate control; part of write | (deny file-write-unlink) |
| Deny within allow | Does NOT work (allow is absolute) | Works (specificity wins) |
| Symlink pairs | Only resolved path used | Both symlink + target get rules |
platform_rules |
Ignored | Injected verbatim in Seatbelt profile |
| Network blocking | Landlock ABI V4+ (TCP only) | Always enforced |
| Capability expansion | seccomp-notify (kernel 5.14+) | Disabled (DYLD issues with SIP/arm64) |
| Learn mode | strace-based | Not supported |
# Test directories
mkdir -p ~/nono-qa/inside ~/nono-qa/outside ~/nono-qa/profile-test ~/nono-qa/undo-test
# Test files
echo "inside-file" > ~/nono-qa/inside/data.txt
echo "outside-file" > ~/nono-qa/outside/secret.txt
echo "profile-data" > ~/nono-qa/profile-test/notes.txt
echo "file-two" > ~/nono-qa/outside/other.txt
# Binary file for undo testing
dd if=/dev/urandom of=~/nono-qa/undo-test/binary.bin bs=1024 count=1 2>/dev/null
echo "original-content" > ~/nono-qa/undo-test/tracked.txt
# User profile directory
mkdir -p ~/.config/nono/profilesCore kernel-level isolation via Landlock (Linux) and Seatbelt (macOS).
Goal: Verify that an explicitly allowed path is accessible inside the sandbox.
nono run --allow ~/nono-qa/inside -- cat ~/nono-qa/inside/data.txtExpected:
catsucceeds, printsinside-file- Exit code 0
Goal: Verify that a path not in the capability set is denied.
nono run --allow ~/nono-qa/inside -- cat ~/nono-qa/outside/secret.txtExpected:
catfails with "Operation not permitted" or "Permission denied"- Exit code non-zero
nono run --read ~/nono-qa/inside -- cat ~/nono-qa/inside/data.txtExpected: cat succeeds (read allowed).
nono run --read ~/nono-qa/inside -- sh -c 'echo "overwrite" > ~/nono-qa/inside/data.txt'Expected: Write fails -- --read grants Read-only.
touch ~/nono-qa/outside/write-target.txt
nono run --write ~/nono-qa/outside -- sh -c 'echo "written" > ~/nono-qa/outside/write-target.txt'Expected: Write succeeds.
nono run --write ~/nono-qa/outside -- cat ~/nono-qa/outside/secret.txtExpected: Read fails -- --write does not grant read access.
nono run --read-file ~/nono-qa/outside/secret.txt -- cat ~/nono-qa/outside/secret.txtExpected: cat succeeds -- file-level read granted.
nono run --read-file ~/nono-qa/outside/secret.txt -- cat ~/nono-qa/outside/other.txtExpected: cat fails -- only the specific file was granted, not the directory.
nono run --net-block -- curl -s --max-time 5 https://example.comExpected:
curlfails with a network error- On macOS: Seatbelt
(deny network*)blocks all sockets - On Linux: Landlock ABI V4+ blocks TCP; if ABI < V4, warning printed and network NOT blocked
nono run -- curl -s --max-time 5 https://example.com | head -5Expected: Network succeeds -- allowed by default without --net-block.
nono run --allow ~/nono-qa/inside -- sh -c 'cat ~/nono-qa/inside/data.txt && cat ~/nono-qa/outside/secret.txt'Expected:
- First
catsucceeds - Second
catfails - The child cannot expand its own sandbox after
restrict_self()/sandbox_init()
# macOS only
echo "test" > /tmp/nono-qa-symlink-test.txt
nono run --allow /tmp -- cat /tmp/nono-qa-symlink-test.txtExpected: cat succeeds -- Seatbelt profile has rules for both /tmp and /private/tmp.
nono run --allow /tmp --dry-run -- true 2>&1Expected: Dry-run output shows both symlink and target paths.
nono run -- cat /etc/hostsExpected: Succeeds -- /etc is in system_read_macos with symlink pair to /private/etc.
nono setup 2>&1Expected on Linux:
- Shows detected Landlock ABI version (V1-V5)
- Shows which features are available at each ABI level
- If Landlock not available: actionable error message with boot parameter instructions
Group-based policy, profiles, CLI overrides, and capability resolution.
Goal: Verify base groups are always applied.
nono run -- cat /usr/share/zsh/5.9/functions/_ls 2>/dev/null | head -3(Adjust path to any readable file under /usr/share/.)
Expected: Succeeds -- /usr is in system_read_macos / system_read_linux.
nono run -- sh -c 'echo test > /tmp/nono-qa-write-test.txt && cat /tmp/nono-qa-write-test.txt && rm /tmp/nono-qa-write-test.txt'Expected: Succeeds -- /tmp is in system_write_* groups.
mkdir -p ~/.aws && echo "test-cred" > ~/.aws/nono-qa-test
nono run -- cat ~/.aws/nono-qa-test
rm ~/.aws/nono-qa-testExpected: cat fails -- ~/.aws is in deny_credentials group.
nono run -- ls ~/.ssh/Expected: Fails -- ~/.ssh is in deny_credentials.
nono run -- cat ~/.zsh_history 2>&1
nono run -- cat ~/.bash_history 2>&1Expected: Both fail -- deny_shell_history group.
# macOS
nono run -- ls ~/Library/Application\ Support/Google/Chrome/ 2>&1
# Linux
nono run -- ls ~/.config/google-chrome/ 2>&1Expected: Fails -- deny_browser_data_* groups.
echo "expendable" > /tmp/nono-qa-expendable.txt
nono run --allow /tmp -- rm /tmp/nono-qa-expendable.txtExpected: rm blocked -- in dangerous_commands group. File still exists.
nono run -- dd if=/dev/zero of=/dev/null bs=1 count=1Expected: dd blocked.
echo "expendable" > /tmp/nono-qa-expendable.txt
nono run --allow /tmp --allow-command rm -- rm /tmp/nono-qa-expendable.txtExpected: rm succeeds -- --allow-command rm overrides the blocklist.
nono run --allow-cwd --block-command ls -- ls ~/nono-qa/inside/Expected: ls blocked -- added by --block-command.
# macOS only
nono run -- diskutil list 2>&1Expected: diskutil blocked -- in dangerous_commands_macos.
# Linux only
nono run -- ls /private/etc/ 2>&1Expected: /private/etc does not exist; system_read_macos group was skipped.
nono run --dry-run -- true 2>&1Expected: deny_credentials resolved on both platforms; ~/.ssh, ~/.aws in deny paths.
nono run -- ls ~/.cargo/bin/ 2>/dev/null | head -3Expected: Succeeds if ~/.cargo/bin exists -- ~ expanded during group resolution.
nono run -- sh -c 'echo $TMPDIR && ls $TMPDIR | head -3'Expected: $TMPDIR accessible -- expanded in system write groups.
nono run --dry-run -vv -- true 2>&1 | grep -i "skip\|non-existent\|does not exist"Expected: Debug warnings for non-existent paths; no errors.
nono run --profile claude-code --dry-run -- true 2>&1Expected:
- Shows resolved groups (base + profile groups including node_runtime, rust_runtime, etc.)
- Deny rules for credentials, browser data, keychains
- Blocked commands
- Network allowed
- Profile filesystem entries:
~/.claude,~/.vscode, etc.
Expected from above dry-run:
$HOME/.claudein allowed paths (profilefilesystem.allow)$HOME/.gitconfigin read paths (profilefilesystem.read_file)- Additive on top of group-resolved paths
cd ~/nono-qa/inside
nono run --profile claude-code --allow-cwd -- cat data.txtExpected: Succeeds -- claude-code sets workdir.access = "readwrite", --allow-cwd skips prompt.
cat > ~/.config/nono/profiles/qa-test.toml << 'EOF'
[meta]
name = "qa-test"
version = "1.0.0"
description = "QA test profile"
[security]
groups = ["node_runtime", "rust_runtime"]
[filesystem]
allow = ["$HOME/nono-qa/profile-test"]
read = []
write = []
allow_file = []
read_file = []
write_file = []
[network]
block = false
[workdir]
access = "read"
EOFnono run --profile qa-test --allow-cwd --dry-run -- true 2>&1Expected: Base groups + node_runtime + rust_runtime; ~/nono-qa/profile-test in allowed paths; CWD with Read access.
cd ~/nono-qa/inside
nono run --profile qa-test --allow-cwd -- cat ~/nono-qa/profile-test/notes.txtExpected: Succeeds, prints profile-data.
cd ~/nono-qa/inside
nono run --profile qa-test --allow-cwd -- cat ~/nono-qa/outside/secret.txtExpected: Fails -- ~/nono-qa/outside not in any group or profile entry.
cd ~/nono-qa/inside
nono run --profile qa-test --allow-cwd --allow ~/nono-qa/outside -- cat ~/nono-qa/outside/secret.txtExpected: Succeeds -- CLI --allow adds ReadWrite.
cd ~/nono-qa/inside
nono run --profile qa-test --allow-cwd --read ~/nono-qa/outside -- cat ~/nono-qa/outside/secret.txtExpected: Read succeeds.
cd ~/nono-qa/inside
nono run --profile qa-test --allow-cwd --read ~/nono-qa/outside -- sh -c 'echo "x" > ~/nono-qa/outside/secret.txt'Expected: Write fails.
nono run --profile qa-test --allow-cwd --allow ~/nono-qa/profile-test --dry-run -v -- true 2>&1Expected: ~/nono-qa/profile-test already covered by profile; CLI override skipped.
cd ~/nono-qa/inside
nono run --profile qa-test --allow-cwd --read-file ~/nono-qa/outside/secret.txt -- cat ~/nono-qa/outside/secret.txtExpected: Succeeds -- --read-file bypasses directory coverage check.
cd ~/nono-qa/inside
nono run --profile qa-test --allow-cwd --net-block -- curl -s --max-time 5 https://example.com 2>&1Expected: Fails -- --net-block overrides profile's network.block = false.
nono run --read ~/nono-qa/inside --write ~/nono-qa/inside --dry-run -- true 2>&1Expected: ~/nono-qa/inside appears once with ReadWrite (merged).
nono run --profile claude-code --read /tmp --dry-run -- true 2>&1Expected: /tmp is ReadWrite from groups but User --read wins; shows Read-only.
# macOS only
nono run --allow /tmp --dry-run -- true 2>&1Expected: Both /tmp and /private/tmp appear in output.
echo "expendable" > ~/nono-qa/inside/expendable.txt
nono run --profile claude-code --allow ~/nono-qa/inside --allow-cwd --allow-command rm -- rm ~/nono-qa/inside/expendable.txtExpected on macOS: Deletion succeeds -- apply_unlink_overrides() adds (allow file-write-unlink) for writable dirs.
Recreate: echo "expendable" > ~/nono-qa/inside/expendable.txt
# Linux only
echo "expendable" > ~/nono-qa/inside/expendable.txt
nono run --allow ~/nono-qa/inside --allow-command rm -- rm ~/nono-qa/inside/expendable.txtExpected: Succeeds -- Landlock write access implies deletion.
cat > ~/.config/nono/profiles/qa-empty-groups.toml << 'EOF'
[meta]
name = "qa-empty-groups"
version = "1.0.0"
description = "Profile with empty groups"
[security]
groups = []
[filesystem]
allow = ["$HOME/nono-qa/inside"]
read = []
write = []
allow_file = []
read_file = []
write_file = []
[network]
block = false
[workdir]
access = "none"
EOFnono run --profile qa-empty-groups -- cat ~/nono-qa/inside/data.txtExpected: Succeeds -- empty groups falls back to base_groups.
nono run --profile qa-empty-groups -- cat ~/.ssh/id_rsa 2>&1Expected: Denied -- base group deny_credentials still applies.
cat > ~/.config/nono/profiles/qa-bad-group.toml << 'EOF'
[meta]
name = "qa-bad-group"
version = "1.0.0"
description = "Profile with unknown group"
[security]
groups = ["nonexistent_group_name"]
[filesystem]
allow = []
read = []
write = []
allow_file = []
read_file = []
write_file = []
[network]
block = false
[workdir]
access = "none"
EOFnono run --profile qa-bad-group -- true 2>&1Expected: Error: Unknown policy group: 'nonexistent_group_name'; exit non-zero; sandbox NOT applied (config failure is fatal).
# Linux only
nono run --allow ~/.ssh --dry-run -v -- true 2>&1Expected: Warning that ~/.ssh is in deny paths but also in allow paths.
# macOS only
nono run --allow ~ -- cat ~/.ssh/id_rsa 2>&1Expected on macOS: Fails -- Seatbelt deny for ~/.ssh is more specific than allow for ~.
Expected on Linux: Succeeds -- Landlock allow-list includes ~/.ssh as child of ~.
cat > ~/.config/nono/profiles/qa-multi-runtime.toml << 'EOF'
[meta]
name = "qa-multi-runtime"
version = "1.0.0"
description = "Multiple runtime groups"
[security]
groups = ["node_runtime", "rust_runtime", "python_runtime"]
[filesystem]
allow = []
read = []
write = []
allow_file = []
read_file = []
write_file = []
[network]
block = false
[workdir]
access = "none"
EOFnono run --profile qa-multi-runtime --dry-run -- true 2>&1Expected: All three runtime groups resolved; Node, Rust, Python paths accessible; deny rules still applied.
nono run --profile qa-multi-runtime -- cat ~/.ssh/id_rsa 2>&1Expected: Denied -- runtime groups do not remove deny_credentials from base groups.
Direct, Monitor, and Supervised modes.
Goal: Verify direct exec replaces the nono process.
nono run --allow ~/nono-qa/inside --exec -- cat ~/nono-qa/inside/data.txtExpected:
catsucceeds, printsinside-file- nono process is replaced by
cat(nono PID becomes cat PID) - No diagnostic footer on failure (nono is gone)
- Exit code passes through
nono run --allow ~/nono-qa/inside --exec -- cat ~/nono-qa/outside/secret.txtExpected: cat fails; NO [nono] diagnostic footer (nono exec'd away).
Goal: Verify monitor mode sandboxes both parent and child, pipes output, and injects diagnostic footer.
nono run --allow ~/nono-qa/inside -- cat ~/nono-qa/inside/data.txtExpected:
catsucceeds- Both parent and child are sandboxed
- Output piped through parent
nono run --allow ~/nono-qa/inside -- cat ~/nono-qa/outside/secret.txtExpected:
catfails- Diagnostic footer appears with
[nono]prefix - Footer shows sandbox policy (allowed paths, network status)
- Footer includes
--allowsuggestions
nono run --allow ~/nono-qa/inside --no-diagnostics -- cat ~/nono-qa/outside/secret.txtExpected: cat fails; NO diagnostic footer.
Goal: Verify supervised mode: parent unsandboxed, child sandboxed, IPC active.
nono run --supervised --allow ~/nono-qa/inside -- cat ~/nono-qa/inside/data.txtExpected:
- Banner shows supervised mode
catsucceeds- Exit code 0
nono run --supervised --allow ~/nono-qa/inside -- cat ~/nono-qa/outside/secret.txtExpected (Linux): seccomp-notify intercepts; approval prompt appears. Expected (macOS): Seatbelt denies directly; diagnostic footer appears (no expansion).
Goal: Verify signals are forwarded from parent to child.
nono run --allow ~/nono-qa/inside -- sleep 30 &
NONO_PID=$!
sleep 1
kill -INT $NONO_PID
wait $NONO_PID
echo "Exit code: $?"Expected:
- SIGINT forwarded to
sleep - nono exits with 128+2 = 130
nono run --allow ~/nono-qa/inside -- sh -c 'exit 42'
echo "Exit code: $?"Expected: Exit code is 42.
nono run --allow ~/nono-qa/inside -- sh -c 'exit 0'
echo "Exit code: $?"Expected: Exit code is 0.
Goal: Verify dangerous environment variables are stripped.
LD_PRELOAD=/evil/lib.so DYLD_INSERT_LIBRARIES=/evil/lib.dylib BASH_ENV=/evil/script nono run --allow ~/nono-qa/inside -- env 2>/dev/null | grep -E "LD_PRELOAD|DYLD_INSERT|BASH_ENV"Expected: None of the dangerous variables appear in the child environment.
HOME=/Users/test PATH=/usr/bin TERM=xterm nono run --allow ~/nono-qa/inside -- env 2>/dev/null | grep -E "^HOME=|^PATH=|^TERM="Expected: Safe variables (HOME, PATH, TERM) are preserved.
Transparent capability expansion via seccomp-notify. Linux-only for expansion; macOS gets diagnostic footer and undo only.
nono run --supervised --allow ~/nono-qa/inside -- cat ~/nono-qa/outside/secret.txtExpected sequence:
catcallsopenat()for the file- seccomp BPF traps the syscall before Landlock
- Supervisor reads path from
/proc/PID/mem - Approval prompt appears:
[nono] The sandboxed process is requesting additional access: [nono] Path: /Users/<you>/nono-qa/outside/secret.txt [nono] Access: read-only [nono] [nono] Grant access? [y/N] - Type
y - Supervisor opens file, injects fd via
SECCOMP_IOCTL_NOTIF_ADDFD catprintsoutside-file(zero retries)- Exit code 0
nono run --supervised --allow ~/nono-qa/inside -- cat ~/nono-qa/outside/secret.txtExpected:
- Prompt appears
- Type
n(or press Enter -- default is deny) catsees EPERM, fails- Diagnostic footer appears
- Exit non-zero
nono run --supervised --allow ~/nono-qa/inside -- cat /etc/shadowExpected: No prompt (never_grant blocks immediately); cat fails.
nono run --supervised --allow ~/nono-qa/inside -- cat /etc/passwdExpected (Linux): Denied without prompt.
Expected (macOS): Succeeds -- /etc in system_read_macos; never_grant not enforced without expansion layer.
mkdir -p ~/.ssh && touch ~/.ssh/authorized_keys
nono run --supervised --allow ~/nono-qa/inside -- cat ~/.ssh/authorized_keysExpected:
- macOS: Seatbelt deny rule blocks at kernel level
- Linux: seccomp intercepts; never_grant denies without prompt
nono run --supervised --allow ~/nono-qa/inside -- cat ~/nono-qa/inside/data.txtExpected:
- Succeeds immediately with NO approval prompt
- On Linux: seccomp intercepts but supervisor recognizes path in initial set (HashSet fast-path)
- On macOS: Seatbelt allows directly
nono run --supervised --allow ~/nono-qa/inside -- sh -c "cat ~/nono-qa/outside/secret.txt && cat ~/nono-qa/outside/other.txt"Expected:
- Prompt for
secret.txt-- approve - Prints
outside-file - Prompt for
other.txt-- approve - Prints
file-two - Each file requires its own approval
Variation -- deny second file:
nono run --supervised --allow ~/nono-qa/inside -- sh -c "cat ~/nono-qa/outside/secret.txt; cat ~/nono-qa/outside/other.txt"Approve first, deny second. First succeeds, second fails.
nono run --supervised --allow ~/nono-qa/inside -- sh -c "echo 'written' > ~/nono-qa/outside/new-write.txt"Expected: Fails -- open_path_for_access() uses create(false); non-existent files cannot be created via expansion.
touch ~/nono-qa/outside/writable.txt
nono run --supervised --allow ~/nono-qa/inside -- sh -c "echo 'written' > ~/nono-qa/outside/writable.txt"Expected: Prompt appears; approve; write succeeds. Verify: cat ~/nono-qa/outside/writable.txt shows written.
for i in $(seq 1 20); do echo "file $i" > ~/nono-qa/outside/rapid-$i.txt; done
nono run --supervised --allow ~/nono-qa/inside -- sh -c '
for i in $(seq 1 20); do
cat ~/nono-qa/outside/rapid-$i.txt 2>/dev/null &
done
wait
'Expected:
- First several requests (burst capacity: 5) may produce prompts
- Subsequent rapid requests auto-denied as "rate limited"
- Far fewer than 20 prompts appear
echo "cat ~/nono-qa/outside/secret.txt" | nono run --supervised --allow ~/nono-qa/inside -- shExpected:
TerminalApprovalreads from/dev/tty, not stdin- If
/dev/ttyavailable: prompt appears despite piped stdin - If no tty (CI/cron): auto-denied with "No terminal available"
Content-addressable snapshots with merkle verification. Requires --supervised mode.
nono run --supervised --allow ~/nono-qa/undo-test --no-undo-prompt -- sh -c 'echo "modified" > ~/nono-qa/undo-test/tracked.txt'Expected:
- Baseline snapshot taken before command runs
- Final snapshot taken after command exits
- Post-exit summary: "1 file changed (1 modified)"
--no-undo-promptskips the interactive restore question
nono run --supervised --allow ~/nono-qa/undo-test -- sh -c '
echo "new-file" > ~/nono-qa/undo-test/created.txt
echo "modified" > ~/nono-qa/undo-test/tracked.txt
rm ~/nono-qa/undo-test/binary.bin
'Expected post-exit summary:
+ created.txt(created)~ tracked.txt(modified)- binary.bin(deleted)- Undo prompt appears (if interactive)
After 5.2, the undo prompt asks to restore:
Expected when answering y:
created.txtremoved (did not exist at baseline)tracked.txtrestored tooriginal-contentbinary.binrestored from object store- Merkle root verified
nono undo listExpected: Shows recent sessions with ID, timestamp, command, and tracked paths.
nono undo list --json | head -1
# Use the session_id from above
nono undo show <SESSION_ID>Expected: Shows session metadata, snapshot timeline, merkle roots.
nono undo restore <SESSION_ID> --dry-runExpected: Shows what would change without modifying disk.
nono undo verify <SESSION_ID>Expected: "Integrity verified" -- merkle root matches recomputed tree from manifest.
nono undo cleanup --keep 5 --dry-runExpected: Shows which sessions would be pruned without deleting anything.
nono undo cleanup --older-than 0 --dry-runExpected: Shows all sessions as candidates for cleanup.
Goal: Verify excluded files are not tracked in snapshots.
mkdir -p ~/nono-qa/undo-test/.git/objects
echo "git-object" > ~/nono-qa/undo-test/.git/objects/abc123
echo "ds-store" > ~/nono-qa/undo-test/.DS_Store
nono run --supervised --allow ~/nono-qa/undo-test --no-undo-prompt -- sh -c '
echo "modified" > ~/nono-qa/undo-test/.git/objects/abc123
echo "modified" > ~/nono-qa/undo-test/.DS_Store
echo "modified" > ~/nono-qa/undo-test/tracked.txt
'Expected:
.git/objectsand.DS_Storechanges NOT listed in change summary- Only
tracked.txtmodification shown - Base exclusions:
.git/objects,.DS_Store
nono run --supervised --allow ~/nono-qa/undo-test --read ~/nono-qa/outside --no-undo-prompt -- sh -c 'echo done'Expected:
- Only
~/nono-qa/undo-testtracked (writable, User source) ~/nono-qa/outsideNOT tracked (read-only)- System paths NOT tracked (Group source)
ln -sf ~/nono-qa/outside/secret.txt ~/nono-qa/undo-test/link-to-outside.txt
nono run --supervised --allow ~/nono-qa/undo-test --no-undo-prompt -- trueExpected: Symlink link-to-outside.txt is NOT followed during snapshot walk; only regular files tracked.
# Create identical files
echo "same-content" > ~/nono-qa/undo-test/file-a.txt
echo "same-content" > ~/nono-qa/undo-test/file-b.txt
nono run --supervised --allow ~/nono-qa/undo-test --no-undo-prompt -- trueExpected: Object store contains only one blob for both files (SHA-256 dedup). Verify by checking ~/.nono/undo/<SESSION_ID>/objects/ -- both files map to the same hash path.
On macOS APFS, the object store uses clonefile() for instant copy-on-write. No direct QA verification needed beyond confirming snapshots work (5.1-5.3), but performance should be noticeably faster on APFS than HFS+.
Error output when sandbox denials occur.
nono run --allow ~/nono-qa/inside -- cat ~/nono-qa/outside/secret.txtExpected:
catfails- Footer appears with
[nono]prefix on every line - Shows: "Command exited with code N. This may be due to sandbox restrictions."
- Shows sandbox policy summary (user paths listed, system paths summarized with count)
- Shows help:
--allow <path>,--read <path>,--write <path>
nono run --supervised --allow ~/nono-qa/inside -- cat ~/nono-qa/outside/secret.txt(On macOS, deny immediately. On Linux, deny at prompt.)
Expected:
- Footer printed after child exits
- On Linux with denials: groups by reason (PolicyBlocked, UserDenied)
- On macOS: standard policy summary + re-run suggestions
nono run --allow ~/nono-qa/inside -- sh -c '
cat ~/nono-qa/outside/secret.txt 2>&1
cat ~/nono-qa/outside/other.txt 2>&1
'Expected: Footer appears once (debounced 2s minimum between injections), not twice.
nono run --allow ~/nono-qa/inside -v -- cat ~/nono-qa/inside/data.txtExpected at -v: All paths shown with source labels ([user], [group:name]).
nono run --allow ~/nono-qa/inside -- cat ~/nono-qa/inside/data.txtExpected at default: Only user/profile paths shown; system/group paths hidden with count.
nono run --allow ~/nono-qa/inside -s -- cat ~/nono-qa/inside/data.txtExpected: No banner, no diagnostic output, no prompts. Only command stdout/stderr.
Permission checking without executing commands.
nono why --allow ~/nono-qa/inside --path ~/nono-qa/inside --op readExpected: "ALLOWED" -- path is in the capability set.
nono why --allow ~/nono-qa/inside --path ~/nono-qa/outside --op readExpected: "DENIED" -- path not in capability set.
nono why --profile claude-code --path ~/.claude --op readwriteExpected: "ALLOWED" -- ~/.claude in profile filesystem entries.
nono why --profile claude-code --path ~/.ssh --op readExpected: "DENIED" -- ~/.ssh in deny_credentials group.
nono why --path ~/.ssh --op readExpected: Reports denied with explanation (sensitive/deny group).
nono why --host example.com --port 443Expected: "ALLOWED" -- network not blocked by default.
nono why --net-block --host example.com --port 443Expected: "DENIED" -- network blocked.
nono why --allow ~/nono-qa/inside --path ~/nono-qa/inside --op read --jsonExpected: Structured JSON with status field.
nono run --allow ~/nono-qa/inside -- sh -c 'nono why --self --path ~/nono-qa/inside --op read'Expected: Reads from NONO_CAP_FILE state file; reports "ALLOWED".
nono run --allow ~/nono-qa/inside -- sh -c 'nono why --self --path ~/nono-qa/outside --op read'Expected: Reports "DENIED".
Working directory inclusion behavior.
cd ~/nono-qa/inside
nono run -- ls .Expected: Prompt asks about sharing CWD. Answer y: ls succeeds. Answer n: ls may fail.
cd ~/nono-qa/inside
nono run --allow-cwd -- ls .Expected: No prompt; ls succeeds.
cat > ~/.config/nono/profiles/qa-readonly-cwd.toml << 'EOF'
[meta]
name = "qa-readonly-cwd"
version = "1.0.0"
description = "Read-only CWD"
[security]
groups = []
[filesystem]
allow = []
read = []
write = []
allow_file = []
read_file = []
write_file = []
[network]
block = false
[workdir]
access = "read"
EOFcd ~/nono-qa/inside
nono run --profile qa-readonly-cwd --allow-cwd -- cat data.txtExpected: Read succeeds.
cd ~/nono-qa/inside
nono run --profile qa-readonly-cwd --allow-cwd -- sh -c 'echo "new" > new-file.txt'Expected: Write fails -- CWD has read-only access.
cat > ~/.config/nono/profiles/qa-no-cwd.toml << 'EOF'
[meta]
name = "qa-no-cwd"
version = "1.0.0"
description = "No CWD access"
[security]
groups = []
[filesystem]
allow = []
read = []
write = []
allow_file = []
read_file = []
write_file = []
[network]
block = false
[workdir]
access = "none"
EOFcd ~/nono-qa/inside
nono run --profile qa-no-cwd -- cat data.txtExpected: No CWD prompt; cat data.txt fails -- CWD not added.
cat > ~/.config/nono/profiles/qa-vars.toml << 'EOF'
[meta]
name = "qa-vars"
version = "1.0.0"
description = "Variable expansion test"
[security]
groups = []
[filesystem]
allow = ["$HOME/nono-qa/inside"]
read = []
write = []
allow_file = []
read_file = []
write_file = []
[network]
block = false
[workdir]
access = "none"
EOFnono run --profile qa-vars -- cat ~/nono-qa/inside/data.txtExpected: $HOME expanded; cat succeeds.
cat > ~/.config/nono/profiles/qa-workdir-var.toml << 'EOF'
[meta]
name = "qa-workdir-var"
version = "1.0.0"
description = "Workdir variable test"
[security]
groups = []
[filesystem]
allow = ["$WORKDIR"]
read = []
write = []
allow_file = []
read_file = []
write_file = []
[network]
block = false
[workdir]
access = "none"
EOFcd ~/nono-qa/inside
nono run --profile qa-workdir-var --workdir ~/nono-qa/inside -- cat data.txtExpected: $WORKDIR expanded to ~/nono-qa/inside; cat succeeds.
nono run --allow ~/nono-qa/inside -- sh -c 'ls -la $NONO_CAP_FILE'Expected:
- File exists at
/tmp/.nono-<PID>.json - Permissions: 0600
NONO_CAP_FILEenvironment variable set
nono run --allow ~/nono-qa/inside -- sh -c 'cat $NONO_CAP_FILE'Expected: JSON with fs array (path, access, is_file), net_blocked, allowed_commands, blocked_commands.
nono run --allow ~/nono-qa/inside -- true
ls /tmp/.nono-*.json 2>/dev/nullExpected: No stale state files for completed processes.
nono setupExpected:
- Phase 1: Installation check (binary path, version, platform)
- Phase 2: Sandbox support test
- macOS: Seatbelt verified via fork+sandbox_init
- Linux: Landlock ABI detected, features listed
- Phase 3: Protection summary (sensitive paths count, dangerous commands)
- Phase 4: Built-in profiles listed
- Quick start examples
On a system without Landlock (old kernel, not enabled):
Expected: Clear error with instructions (e.g., boot parameters for Landlock).
strace-based path discovery.
# Linux only
nono learn -- cat ~/nono-qa/inside/data.txtExpected: Shows paths accessed by cat, categorized as read/write.
# Linux only
nono learn --profile claude-code -- cat ~/nono-qa/inside/data.txtExpected: Only shows paths NOT covered by the claude-code profile. System paths filtered out.
# Linux only
nono learn --json -- cat ~/nono-qa/inside/data.txtExpected: Profile-fragment JSON with filesystem: {allow: [], read: [], write: []}.
nono run --profile claude-code --allow-cwd -- true
ls -la ~/.claude/hooks/nono-hook.shExpected:
- Hook script installed at
~/.claude/hooks/nono-hook.sh - Executable (755)
cat ~/.claude/CLAUDE.md | grep -A 5 "nono-sandbox-start"Expected: <!-- nono-sandbox-start --> section present with sandbox instructions.
cat ~/.claude/hooks/nono-hook.sh | head -20Expected: Script reads NONO_CAP_FILE and uses jq to parse capability JSON.
nono run --allow ~/nono-qa/inside --read ~/nono-qa/outside --net-block --dry-run -- true 2>&1Expected: Shows capabilities, deny rules, blocked commands, network status. Does NOT execute. Exit 0.
nono run --profile claude-code --dry-run -- true 2>&1Expected: Profile name, resolved groups, all filesystem entries, deny rules, blocked commands, network policy.
nono run --profile claude-code --allow ~/nono-qa/outside --block-command ls --dry-run -- true 2>&1Expected: Three-layer composition visible: groups + profile + CLI overrides.
mkdir -p ~/nono-qa/"path with spaces"
echo "spaced" > ~/nono-qa/"path with spaces"/file.txt
nono run --allow ~/nono-qa/"path with spaces" -- cat ~/nono-qa/"path with spaces"/file.txtExpected: Succeeds -- paths with spaces handled correctly.
mkdir -p ~/nono-qa/'special-$chars'
echo "special" > ~/nono-qa/'special-$chars'/file.txt
nono run --allow ~/nono-qa/'special-$chars' -- cat ~/nono-qa/'special-$chars'/file.txtExpected: Succeeds -- special characters in paths handled correctly.
nono run --allow ~/nono-qa/nonexistent -- true 2>&1Expected: Error -- path does not exist; FsCapability::new_dir() fails with PathNotFound.
nono run --allow ~/nono-qa/inside/data.txt -- true 2>&1Expected: Error -- --allow expects a directory; ExpectedDirectory error.
nono run --allow-file ~/nono-qa/inside -- true 2>&1Expected: Error -- --allow-file expects a file; ExpectedFile error.
Paths containing control characters (0x00-0x1F, 0x7F) must be rejected to prevent Seatbelt profile injection. This is enforced at the library level in escape_path() and covered by unit tests.
nono run --allow ~/nono-qa/inside -- 2>&1Expected: Error -- no command specified.
nono run --allow ~/nono-qa/inside -- nonexistent_command_12345 2>&1
echo "Exit: $?"Expected: Error; exit code 127 (execve failed).
# This should fail: supervised mode rejects keyring threads (deadlock risk)
nono run --supervised --allow ~/nono-qa/inside --secrets test_key -- true 2>&1Expected: Error about threading context incompatibility (supervised requires single-threaded for fork safety; keyring backend creates threads).
# First, add a test secret to the system keystore:
# macOS: security add-generic-password -a nono_qa_test -s nono -w "test-secret-value"
# Linux: secret-tool store --label='nono test' service nono username nono_qa_test <<< "test-secret-value"
nono run --allow ~/nono-qa/inside --secrets nono_qa_test -- sh -c 'echo $NONO_QA_TEST'Expected: Prints test-secret-value -- secret loaded from system keystore and injected as env var (uppercased name).
nono run --allow ~/nono-qa/inside --secrets nonexistent_key_12345 -- true 2>&1Expected: Error: SecretNotFound for the missing key.
On macOS, keyring access requires Mach IPC which the sandbox blocks. Secrets must be loaded before sandbox_init().
Verify: If 16.1 works on macOS, secrets are being loaded pre-sandbox.
rm -rf ~/nono-qa
rm -f ~/.config/nono/profiles/qa-test.toml
rm -f ~/.config/nono/profiles/qa-empty-groups.toml
rm -f ~/.config/nono/profiles/qa-bad-group.toml
rm -f ~/.config/nono/profiles/qa-readonly-cwd.toml
rm -f ~/.config/nono/profiles/qa-no-cwd.toml
rm -f ~/.config/nono/profiles/qa-vars.toml
rm -f ~/.config/nono/profiles/qa-workdir-var.toml
rm -f ~/.config/nono/profiles/qa-multi-runtime.toml
rm -f /tmp/nono-qa-*| Section | Test | Verify |
|---|---|---|
| 1.1 | Allowed path | cat succeeds |
| 1.2 | Disallowed path | cat fails |
| 1.3 | Read-only | Read OK, write blocked |
| 1.6 | Network block | curl fails with --net-block |
| 1.8 | Symlink pairs | /tmp + /private/tmp both accessible |
| 2.1 | System read | /usr/share readable |
| 2.1 | Deny credentials | ~/.aws blocked by Seatbelt deny |
| 2.1 | Deny shell history | ~/.zsh_history blocked |
| 2.2 | Dangerous commands | rm, diskutil blocked |
| 2.5 | claude-code profile | Groups resolved, deny rules active |
| 2.7 | CLI overrides | --allow, --read, --net-block work |
| 2.9 | Unlink protection | Writable dirs get override rules |
| 2.12 | Deny within allow | ~/.ssh denied with --allow ~ |
| 3.2 | Monitor mode | Diagnostic footer on denial |
| 3.3 | Supervised mode | Banner, sandbox, undo |
| 4.3 | never_grant | ~/.ssh blocked by deny rule |
| 5.1-5.3 | Undo | Snapshot, detect changes, restore |
| 6.1 | Diagnostic footer | [nono] prefix, policy summary |
| 7.2 | Query profile | Source attribution correct |
| 8.3 | CWD read-only | Write to CWD fails |
| 13.1 | Hooks | Claude Code hook installed |
| 15.1 | Path spaces | Handled correctly |
| Section | Test | Verify |
|---|---|---|
| 1.1 | Allowed path | cat succeeds |
| 1.2 | Disallowed path | cat fails (Landlock) |
| 1.3 | Read-only | Read OK, write blocked (Landlock) |
| 1.6 | Network block | ABI V4+: blocked; ABI < V4: warning |
| 1.9 | Landlock ABI | nono setup shows ABI version |
| 2.1 | System read | /usr/share readable |
| 2.1 | Deny credentials | ~/.aws not in allow list |
| 2.2 | Dangerous commands | rm, dd blocked |
| 2.5 | claude-code profile | Groups resolved |
| 2.7 | CLI overrides | --allow, --read, --net-block work |
| 2.9 | Unlink no-op | Deletion works with write access |
| 2.12 | Deny overlap | Warning on deny/allow overlap |
| 3.2 | Monitor mode | Diagnostic footer on denial |
| 3.3 | Supervised mode | Banner, sandbox, IPC |
| 4.1 | Expansion approve | Prompt, y, fd injected |
| 4.2 | Expansion deny | Prompt, n, EPERM |
| 4.3 | never_grant | /etc/shadow denied without prompt |
| 4.4 | Initial set | No prompt for allowed paths |
| 4.5 | Multiple files | Per-file approval |
| 4.7 | Rate limiting | Rapid requests auto-denied |
| 5.1-5.3 | Undo | Snapshot, detect changes, restore |
| 5.5 | Merkle verify | nono undo verify succeeds |
| 6.1 | Diagnostic footer | [nono] prefix, policy summary |
| 7.2 | Query profile | Source attribution correct |
| 8.3 | CWD read-only | Write to CWD fails (Landlock) |
| 12.1 | Learn mode | strace paths categorized |
| 15.1 | Path spaces | Handled correctly |
| Check | Command |
|---|---|
| Kernel 5.14+ | uname -r |
| seccomp-notify | cat /proc/sys/kernel/seccomp/actions_avail contains user_notif |
| Landlock enabled | cat /sys/kernel/security/lsm contains landlock |
| strace (for learn) | which strace |
Do NOT use these as "denied" test targets -- they are in base system groups:
/tmp,/private/tmp--system_write_*/var,/private/var--system_read_macos/usr,/bin,/lib,/sbin--system_read_*/etc,/private/etc--system_read_macos/dev--system_write_*~/Library/Caches,~/Library/Logs--user_caches_macos~/.cargo,~/.rustup--rust_runtime~/.nvm,~/.fnm,~/.npm--node_runtime
Use $HOME/nono-qa/ for all "outside" test paths.