::: claude codex pi
Working inside a nono sandbox
Your granted workspace directory is read+write. Package caches, agent config and a few named files are granted.
Most of the rest of $HOME is not.
Most failures that look like sandbox denials are not. Of the sandbox reports raised on this machine, two thirds turned out to be something else β a tool sandboxing itself, a misread diagnostic, or an unrelated failure with a plausible-looking error string. Treat "the sandbox blocked me" as a hypothesis to test, never a conclusion to report.
Before you conclude anything
1. Did the command actually fail? Check the exit code and the real output first β a denial
line next to a failure does not mean it caused the failure. nono's exit diagnostic prints only
on a non-zero exit: Sandbox denial: N path(s) blocked. lists each path with a Fix flags:
line, and those are often harmless probes β tools walking up from the workdir looking for
config. No path denials were observed during this session. means nothing was blocked and the
failure is elsewhere.
Under Claude and OpenCode, nono runs with --silent: no banner and no exit diagnostic. The
failing command's own Operation not permitted is then your only signal, and the absence of a
nono denial line proves nothing. Codex and Pi print both.
2. Quote the path from the error. If you cannot point at a line of output naming a specific path, you do not have a sandbox problem β you have a guess. Never report a denial with a placeholder path.
3. Verify that exact path:
nono why --self --path /the/path/from/the/error --op read|write|readwrite--self is not optional. Without it, nono why evaluates nono's default profile rather
than the running session and reports DENIED / path_not_granted for almost anything β including
from a shell that is not sandboxed at all. A bare nono why is never evidence.
Two ways nono why still misleads you:
- It misreports grants inside the built-in keychain protection. A
read_filegrant on a path under~/Library/Keychainsis honored by the sandbox whilenono whyreportsDENIED / filesystem_deny. If it says denied but the command works, believe the command. - It resolves real paths. A path that does not exist yet reports
path_not_grantedeven when its parent directory is granted.
When the verdict contradicts the error, trust an actual read or write inside nono run over
either.
4. A denial can arrive as a non-permission errno. A hidden path reports absent to stat
and present to an operation on it:
DENIED exists(): False mkdir: EEXIST open(β¦,'x'): EEXIST link: EPERM
GRANTED exists(): True mkdir: EEXIST open(β¦,'x'): EEXIST link: CREATEDEEXIST alone means nothing β it fires on every path that exists. The disagreement is the
signal. Calls that check permission before existence stay honest, so this only shows up where
the existence check runs first.
The general form: the most legible output can point away from the actual cause. An errno
lying about existence, and a forbidden-sandbox-reinit denial suggesting --allow <path> when
it carries no path, are the same trap.
The most common cause: something under nono starting its own sandbox
Nono blocks sandbox re-initialization for anything running under the profile β usually not
the agent itself but a process it spawned (SwiftPM evaluating Package.swift, xcodebuild's
plugin execution, Chrome's zygote). The giveaway is sandbox-exec: sandbox_apply: Operation not permitted, forbidden-sandbox-reinit in nono's exit diagnostic (Codex and Pi only), or an
error naming a path that nono why --self says is allowed.
The denial carries no path, so no grant can address it β ignore nono's own
--allow <path> suggestion here. Disable the inner sandbox instead:
| tool | flag |
|---|---|
swift build / swift test / swift run |
--disable-sandbox |
xcodebuild |
-IDEPackageSupportDisableManifestSandbox=1 -IDEPackageSupportDisablePluginExecutionSandbox=1 |
| Chrome / Chromium | --no-sandbox |
| Codex | -c sandbox_mode="danger-full-access" |
Most of this is already handled: the wrappers set OTHER_SWIFT_FLAGS and
AGENT_BROWSER_ARGS, and bin/sandbox-shims/xcodebuild appends the two
-IDEPackageSupport⦠flags, which are NSUserDefaults and so reachable only through
argv. A tool that spawns xcodebuild itself therefore works without doing anything.
swift build and swift test still take --disable-sandbox on their own command line.
Known limits β report these, do not try to fix them
- Xcode test targets with a host application (
TEST_HOSTset) cannot run sandboxed: the app launches via LaunchServices, lands outside the sandbox, and its connection back never establishes.swift teston aPackage.swifttarget is unaffected. - A profile change does not reach a running session. Seatbelt applies the policy at process start. If a grant was added after this session began, it is invisible until restart β say so rather than asking for it again.
Denied on purpose β these will not be granted
Each was decided deliberately and measured; the reasoning is in
docs/security-model.md. Name the blocker once, hand over the
command, and continue with what is still possible. Do not propose a grant, do not work around it,
and do not raise it again in a later session.
| Blocked | Why it stays blocked | Give the user this |
|---|---|---|
~/Library/Keychains/login.keychain-db |
Holds the login credentials and codesigning identities. A read grant returns them as plaintext through securityd with no prompt, and outbound network is unrestricted, so readable means exfiltratable. Distribution and Developer ID signing is attributable beyond this machine. | For codesigning, use agent-signing.keychain-db β already granted, and it deliberately holds only Apple Development identities for the teams whose apps agents build on device. It carries no Distribution and no Developer ID certificate, so nothing reachable from a sandbox can sign software that ships off this machine. Anything needing the login keychain runs in a plain terminal. |
~/.ssh |
Private keys. Would let an agent git push and ssh to any host. git push is separately denied for the same reason. |
git push β or the ssh command β themselves. |
~/.local/state/mise/trusted-configs |
mise trust applies a repo's [env], including _.path, to the user's own interactive shell in that directory. That is a route out of the sandbox. |
mise trust, once, in that repo. |
~/.gradle/init.d, ~/.gradle/gradle.properties |
Gradle executes init scripts and applies jvmargs on every build, including the user's unsandboxed ones. |
Nothing to run β these stay denied whether or not the rest of ~/.gradle is granted. |
The shape they share: an agent writes or reads something that the user's own unsandboxed tools later trust. A grant that only fails safe inside the granted workspace is a different question and may well be reasonable β these are not that.
When it is a real denial
Say so plainly, quote the path and the nono why --self verdict, and stop. Adding a grant is
the user's decision: it widens the boundary for every agent on the machine, and some paths that
look like config hold credentials.
Do not:
offer
nono run --allow β¦ornono profile promoteas remedies β profiles are version-controlled and edited at their source, not drafted ad hocrelocate files, weaken a test, or call a binary by another path to get around it
ask a peer session to read the path for you ::: ::: opencode On any EPERM / EACCES / "Operation not permitted" tool or shell failure: run
nono why --path <blocked-path> --op readimmediately and include its output verbatim in your reply. Do NOT suggest sudo, chmod, or system permission changes. Then offer the user exactly these two options:Option A (quick fix):
nono run --allow /path/to/needed -- opencodeOption B (persistent fix): draft a profile to
$XDG_CONFIG_HOME/nono/profile-drafts/<name>.jsonextendingopencode, add the path under"allow"or"read", then tell the user to runnono profile promote <name>and start sessions withnono run --profile <name> -- opencode
Working inside a nono sandbox
The user has launched you with nono run --profile <name> -- opencode. nono enforces filesystem and network limits at the OS level (Landlock on Linux, Seatbelt on macOS). These are kernel-enforced boundaries β retries or workarounds inside opencode cannot grant access that nono hasn't already permitted.
Identifying a sandbox denial
The denial signature is in the failed tool's output:
- "Operation not permitted"
- "Permission denied"
- "EACCES" / "EPERM"
- "landlock"
- "sandbox: deny"
When you see any of these on a file, shell, or tool failure, it is a nono boundary β not macOS TCC, not Full Disk Access, not Unix file permissions. Do NOT suggest:
- System Settings / Privacy & Security
chmod,chown,sudo- "grant Full Disk Access to your terminal"
- Retrying the operation via a different path
Network-egress denials look different: a request to a host that is not on the sandbox allowlist fails as a connection refused, timeout, or TLS/proxy error rather than an EPERM. Those are covered in the Network egress denials section below.
Diagnosing
Run nono why to see exactly why access was denied:
nono why --path /the/blocked/path --op readUse --op write for write-only failures and --op readwrite when the operation needs both.
If NONO_CAP_FILE is set, inspect the full capability set:
cat "$NONO_CAP_FILE"Denials that arrive as a non-permission errno
A denied path is hidden from stat but still exists, so an operation that checks existence before permission reports it as present. The two answers contradict each other:
DENIED exists(): False mkdir: EEXIST open(β¦,'x'): EEXIST link: EPERM
GRANTED exists(): True mkdir: EEXIST open(β¦,'x'): EEXIST link: CREATEDEEXIST on its own means nothing β it fires on every path that exists. The disagreement is the signal. Calls that check permission before existence stay honest, which is why link reports EPERM rather than EEXIST.
A tool that stats a directory, finds nothing, tries to create it and dies on "file exists" has hit a denial, not a stale-state bug. mkdir /path: file exists from a tool reading its own config directory is the usual shape.
Two options to present to the user
Option A β quick fix (one-off)
Exit opencode and restart with the path explicitly allowed:
nono run --allow /path/to/needed -- opencodeUse --read when only read access is needed.
Option B β persistent fix (draft a profile)
The active profile directory $XDG_CONFIG_HOME/nono/profiles/ is read-only from inside the sandbox by design. Drafts are written to $XDG_CONFIG_HOME/nono/profile-drafts/ and the user promotes them out-of-band with nono profile promote.
Write the JSON to $XDG_CONFIG_HOME/nono/profile-drafts/<chosen-name>.json extending the active profile. Minimal example for read-only access:
{
"extends": "opencode",
"meta": { "name": "<chosen-name>", "version": "1.0.0" },
"filesystem": { "read": ["/path/to/needed"] }
}If the user is on a custom intermediate profile (e.g. --profile opencode-with-docs extending opencode), change extends to that profile's name so the new profile inherits all their customisations.
If a user profile of that name already exists, read $XDG_CONFIG_HOME/nono/profiles/<chosen-name>.json first, base your edit on that profile, write the full proposed profile to $XDG_CONFIG_HOME/nono/profile-drafts/<chosen-name>.json, and write a SHA-256 of the base bytes to $XDG_CONFIG_HOME/nono/profile-drafts/<chosen-name>.base.
Filesystem field choices:
"read"β read-only directory or file access"write"β write-only access (rare)"allow"β read+write directory access
For a single file rather than a directory, use "allow_file" / "read_file" / "write_file" instead.
After drafting, tell the user:
Drafted profile <chosen-name>. Run `nono profile promote <chosen-name>` to review and apply, then start sessions with `nono run --profile <chosen-name> -- opencode`.Network egress denials
nono routes outbound traffic through a filtering proxy. When network.block is false but a host allowlist is set, only allowlisted hosts are reachable and every other connection fails β usually as a connection refused, timeout, or TLS/proxy error rather than an EPERM. nono-status lists the reachable hosts under "reachable hosts". Retries, alternate endpoints, proxies, or DNS changes cannot bypass the proxy; it is OS-enforced.
If a host is genuinely needed, present the same two options as for filesystem denials.
Option A β quick fix (one-off)
nono run --allow-domain api.example.com -- opencode--allow-domain is repeatable and accepts a plain hostname for unrestricted access, or a URL with a path glob to restrict to specific endpoints (e.g. https://github.com/org/**).
Option B β persistent fix (draft a profile)
Add the host to network.allow_domain in a profile draft extending the active profile:
{
"extends": "opencode",
"meta": { "name": "<chosen-name>", "version": "1.0.0" },
"network": { "allow_domain": ["api.example.com"] }
}Then tell the user to run nono profile promote <chosen-name> and start sessions with nono run --profile <chosen-name> -- opencode.
Validating the new profile
nono profile promote shows a diff and validates before applying. If the user wants to validate directly:
nono profile validate --draft <chosen-name>Credential injection
The opencode nono profile defines credential routes for common AI providers. nono injects these credentials transparently via its proxy β opencode never sees the raw API key.
Built-in route names: openai, anthropic, gemini, github, gitlab.
The corresponding keychain accounts (env-var shaped) are:
OPENAI_API_KEYβ injected asAuthorization: Bearer β¦toapi.openai.comANTHROPIC_API_KEYβ injected asx-api-key: β¦toapi.anthropic.comGOOGLE_API_KEYβ injected asx-goog-api-key: β¦togenerativelanguage.googleapis.com; opencode sees it asGEMINI_API_KEYGITHUB_TOKENβ injected asAuthorization: token β¦toapi.github.comGITLAB_TOKENβ injected asAuthorization: Bearer β¦togitlab.com/api
Routes are defined in the profile but disabled by default. To enable one, create an extending profile and add the route name to network.credentials:
{
"extends": "opencode",
"meta": { "name": "opencode-with-anthropic", "version": "1.0.0" },
"network": { "credentials": ["anthropic"] }
}Do not read or write API keys directly from inside the sandbox. Prefer nono phantom credential routes. If opencode stores a key in $XDG_CONFIG_HOME/opencode/, it is visible to the sandboxed process β use the proxy route instead.
Detach and attach
nono supports running opencode in a detached session that survives terminal disconnects:
nono run --profile opencode --detach -- opencodenono prints the session ID on start. Reattach from any terminal:
nono attach <session-id>The session ID is also available inside the session as NONO_SESSION_ID. The installed plugin surfaces it in the nono-status command output.
To list active nono sessions:
nono sessionsTo stop a detached session cleanly:
nono stop <session-id>Detached sessions inherit the same sandbox profile as interactive ones β the same filesystem grants, credential routes, and network rules apply.
opencode-specific notes
- opencode state, sessions, config, and cache live under
~/.opencode,$XDG_CONFIG_HOME/opencode,$XDG_CACHE_HOME/opencode,$XDG_DATA_HOME/opencode, and$XDG_STATE_HOME/opencode. The base profile grants all of these read/write. - The plugin at
$XDG_CONFIG_HOME/opencode/plugins/nono-sandbox.tsis symlinked from the pack store. It updates automatically onnono pull. - The skill at
$XDG_CONFIG_HOME/opencode/skills/nono-sandbox/is similarly symlinked. - The
nono-statuscommand (registered by the plugin) shows the active capability set, the network egress allowlist (reachable hosts), enabled credential routes, and the session ID for reattach. - Do not add provider secrets to opencode's own config files. Route them through
network.credentialsin the profile instead.
Path conventions
Path references in this skill use $XDG_CONFIG_HOME. If that variable is not set, substitute ~/.config. nono and opencode both follow the XDG Base Directory Specification.
What you should NOT do
- Do not write the profile yourself unless the user explicitly asks for Option B. Present both options first.
- Do not edit the pack-installed profile at
$XDG_CONFIG_HOME/nono/packages/nolabs-ai/opencode/policy.jsonβ it is overwritten on everynono pull. - Do not retry the failing operation in a different way. The sandbox is OS-enforced; alternative paths, endpoints, or commands hit the same boundary.
- Do not edit registry-managed package files under
$XDG_CONFIG_HOME/nono/packages; create a profile extension instead. :::