pi-permission-modes
Declarative, user-definable permission modes for the pi coding agent: OS-level sandboxing (bubblewrap / sandbox-exec), an allow/ask/deny policy engine, real bash AST gating (tree-sitter), tool hiding, and skill/custom-tool gating.
Package details
Install pi-permission-modes from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-permission-modes- Package
pi-permission-modes- Version
2.4.1- Published
- Sep 24, 2026
- Downloads
- 2,097/mo · 1,268/wk
- Author
- wynainfo
- License
- MIT
- Types
- extension
- Size
- 327.3 KB
- Dependencies
- 3 dependencies · 3 peers
Pi manifest JSON
{
"video": "https://github.com/wynainfo/pi-permission-modes/releases/download/v2.1.0/pi-permission-modes-demo-web.mp4",
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-permission-modes
Switchable, user-definable permission modes for the pi
coding agent. Each mode is a JSON bundle of an OS-level sandbox profile
(bubblewrap / sandbox-exec) plus an allow / ask / deny policy across every
surface (bash, file tools, the project boundary, web search, extension tools,
skills). Bash gating uses a real tree-sitter AST, not a regex.
Four modes ship by default - Default → Plan Mode → Build → YOLO - and you can
retune them or add your own in permission-mode.json.
See SECURITY.md for the threat model, CHANGELOG.md for releases, and CONTRIBUTING.md to hack on it.
Install
From npm (recommended):
pi install npm:pi-permission-modes
or straight from GitHub:
pi install git:github.com/wynainfo/pi-permission-modes
pi install fetches the package and runs npm install for you, so
@anthropic-ai/sandbox-runtime comes along automatically; the pi SDK is
provided by the host. Useful variants:
pi install npm:pi-permission-modes@2.1.1- pin a version (pi updatewon't advance it)pi install -l npm:pi-permission-modes- project-local install (.pi/npm/, shareable with your team)pi update --extensions- pull the latest release later
To try it without adding it to your config, run it directly from a clone:
pi -e ./src/index.ts
On Linux (including WSL2) the sandbox also needs three system packages on
PATH:
# Debian / Ubuntu / WSL2
sudo apt install -y bubblewrap socat ripgrep
bubblewrap- provides thebwrapbinary that confines bash (the package isbubblewrap, the executable isbwrap).socat- network filtering; without it the sandbox fails to init and the sandboxed modes fall back to prompting.ripgrep- providesrg.
Ubuntu 24.04 and newer (desktop and server; WSL2 images usually do not) enable
kernel.apparmor_restrict_unprivileged_userns, which strips the capabilities bubblewrap and the runtime's seccomp helper need. Every sandboxed command then fails withbwrap: ... Operation not permittedand nothing runs. Either disable the restriction (sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0, persist it in/etc/sysctl.d/) or grantusernstobwrapin an AppArmor profile. WSL1 has no user namespaces at all; the sandbox reports it as unsupported.
On macOS the sandbox uses the built-in sandbox-exec - no extra packages.
Native Windows: no sandbox. There is no OS-level sandbox on Windows and this extension does not attempt one. The sandboxed modes degrade to prompting only: every in-project
bashcommand asks for confirmation, the network allowlist is not enforced, andallowWriteonly feeds the prompt bounds. For real containment run pi under WSL2 and follow the Linux instructions above.
Verify
- Start pi in any project - the footer shows a mode indicator, e.g.
Default. - Switch to Build (
alt+muntilBuild, or/perm build):- sandbox active →
Build (sandboxed in project dir); - otherwise →
Build (!) sandbox-runtime missing ...plus a fix-it notification.
- sandbox active →
/sandboxprints the active sandbox configuration (or why it's unavailable).
Modes
Cycle with alt+m or set directly with /perm <mode>. The current mode
is persisted per session (survives /reload, resume, and branch navigation) and
shown in the footer.
Default, Plan Mode, and Build all run in-project bash inside the OS sandbox.
The footer always shows the mode, the network state, and their shortcuts -
e.g. Build (sandboxed in project dir, alt+m) Network: filtered (alt+n) -
with the network chip green while the domain allowlist filters and orange when
open (see Network). Only YOLO is unsandboxed. Labels are plain text
(no icons); alt+m cycles in the order below. The table describes the shipped defaults - every mode is data and can
be retuned, and you can add your own, in permission-mode.json (see
Configuration).
| Mode | Behavior |
|---|---|
| Default | Confirm every bash/edit/write; reads are free. Approved in-project bash runs sandboxed (writes confined to the project). |
| Plan Mode | Planning mode. Reads are free; in-project bash runs sandboxed read-only (writes/deletes fail), so only read commands effectively work. The one mutation allowed without confirmation is creating/editing Markdown (*.md/*.markdown) inside the project - other edit/write are blocked. A system-prompt addition steers the model: for a planning task, write the plan to plan/<YYYY-MM-DD>_<description>.md, render it for review with the show_plan tool, then hand over to you: an Accept / Decline prompt follows the plan (see Approving a plan). |
| Build | Reads, writes, and bash inside the project run with no confirmation; in-project bash runs sandboxed. |
| YOLO | Never prompts, never sandboxes, no protected-path backstop. Can do anything the current user can. |
In every mode except YOLO: out-of-project access prompts; bash that
reaches outside the project or escalates privileges
(su/sudo/doas/pkexec/runuser/setpriv/chroot - detected even
through wrappers like env/nice/xargs and bash -c '…' scripts) prompts,
and runs unsandboxed once you approve it; and edit/write to
protected paths (.git/, .env, dotfiles, … - see
below) are hard-blocked. The mode's sandbox-writable
directories (allowWrite - /tmp/pi by default - plus the session's
scratch directory and the runtime's own /tmp/claude)
count as in-bounds: a temp file there is not an escape, so it neither prompts
nor runs unsandboxed. Neither is an in-project symlink to an outside
executable (a venv's bin/python, a tool shim): running it is what the
project intends, and the sandbox still governs what it may touch. Symlinks to
outside directories or non-executable files stay escapes.
When the sandbox is unavailable (missing dependency, init failure, unsupported platform,
--no-sandbox): Default/Plan/Build show(!) <reason>in the footer, and in-projectbashthat would have been sandboxed instead prompts for confirmation - you are never silently unprotected.
Sandbox violations are reported to the model. When a sandboxed command tries to write outside the mode's writable roots or to reach a host the allowlist refused, its output ends with a
<sandbox_violations>block naming the path or host, so the model can adjust instead of guessing why a step failed. Linux reports refused write attempts (a denied read just returns nothing, there is no failing syscall to observe); macOS reports both through the system sandbox log. The Linux report is best effort in both directions: it can list a write that actually landed in the throwaway tmpfs the sandbox mounts over a read-denied directory (lost when the command ends), and it misses Plan mode's read-only refusals. Lines for/devand/procare dropped: writes there (e.g./dev/shm) succeed inside the sandbox and were misreported.
Sandbox placeholder cleanup: the sandbox runtime write-protects a fixed set of dotfiles/dirs at the project root (
.git/hooks,.gitconfig,.gitmodules,.bashrc/.zshrc/… shell rc files,.ripgreprc,.mcp.json,.vscode,.idea,.claude/{commands,agents}). When one of these is absent, it blocks the path by mounting/dev/nullover the first missing component, and because the project is writable in Default/Build, bubblewrap materializes that mountpoint as a 0-byte, read-only file. The runtime removes these mount points once no sandboxed command is running any more, and re-covers leftovers of a crashed session on its next run. (Up to 2.3.1 the extension also deleted them around every command itself, which could strip the denies from a sandboxed command running in parallel; it no longer does.) Known limitation: inside the sandbox those masks appear as character devices at the project root, sogit statuslists them as untracked andgit add -A/git add .at the root fails with "can only add regular files". Adding files by name works, which is what agents usually do; if a project needsgit add -A, list the names (.bashrc,.bash_profile,.zshrc,.zprofile,.profile,.gitconfig,.gitmodules,.ripgreprc,.mcp.json) in its.git/info/exclude.Git worktrees and submodules (
.gitis a file pointing at the real git dir) sandbox normally on every platform: the runtime protects.git/hooksonly when.gitis a directory. Because the real git dir lies outside the project, the extension makes it and the shared common dir writable inside the sandbox (git needs them for the index, refs, and objects) with theirhooks,config, and pointer files write-denied, and says so at session start;/sandboxlists them. The.gitfile is project content, so it is trusted only for a layout git itself creates: a worktree whose git dir points back at this.gitfile, or a submodule whosecore.worktreeis this project. Anything else (a planted.gitnaming your home,/, or another repository) grants nothing, and the.gitfile itself is write-denied inside the sandbox. In Plan mode the git dirs stay read-only.
Approving a plan
When a run in Plan Mode (any mode whose systemPrompt is "@plan") has
rendered a plan with show_plan, applying it is one action. A show_plan
in any other mode only displays the file. As soon as the model's handoff line is on screen, a prompt
appears:
Plan ready: plan/2026-09-24_feature.md
> Accept: switch to Build and implement it
Decline: keep refining in Plan Mode
- Accept switches to the approve mode (Build by default) and sends a
real user message,
The plan in \plan/...` is approved. Implement it now.`, so the implementing turn starts under Build's system prompt, never under the Plan prompt still in effect. The message is visible in the transcript like anything you type. - Approval covers what you saw: if the plan file changes after it was shown, approving is refused until the model shows it again.
- Decline (or Esc) costs nothing: you stay in Plan Mode and refine by
typing, as before. The plan stays pending, and it is not offered again
until the next
show_plan.
Two fallbacks cover the other ways in:
- Switching into the approve mode by hand (
alt+m,/perm build) while a plan is pending asksImplement plan/... now?; No leaves you in Build with nothing sent. /plan approveswitches and sends without asking;/plan statusnames the pending plan.
The pending plan survives /reload, resume, and branch navigation (a
perm-plan session entry; an approved plan is never offered again).
Headless sessions get no prompts and no automatic switch. Two optional
global config keys tune it:
"plan": {
"approveMode": "build", // any defined mode
"approveMessage": "The plan in `{path}` is approved. Implement it now." // {path} = plan path
}
A project config cannot set them. If approveMode names a mode that does
not exist, approval is off with a one-time warning.
Scratch directory
Every session gets its own scratch directory for temporary files:
/tmp/pi/<session-id>/ on Linux and macOS, <os.tmpdir()>/pi/<session-id>/
on Windows (override the base with the PI_PERMISSION_TMPDIR env var, e.g. for
a noexec /tmp). The awareness section names it, TMPDIR points there inside
bash (sandboxed and unsandboxed runs; Windows also gets TEMP/TMP), and it
is always sandbox-writable and in-bounds - even if a global or project config
drops the shared /tmp/pi base from allowWrite, so the instruction to use it
stays truthful. /sandbox shows the path.
Sandbox-wise, all pi sessions on the host share the /tmp/pi base (it is in
the shipped allowWrite; the base is created sticky and world-writable like
/tmp, each session folder is 0700). Instruction-wise, each session is told
to use only its own folder - including YOLO, which is otherwise unsandboxed but
still gets a short "scratch directory" pointer. Plan mode's read-only bash can't
write there (like everywhere else), so it isn't advertised there.
Nothing is deleted at shutdown - /reload and session resume find their files
again (the folder is keyed on pi's session id). Instead, at every session start,
sibling folders untouched for 7 days are removed; the current folder is
touched on start so a long-lived session isn't swept by a peer. Files and
symlinks under the base are never touched.
Network
In the sandboxed modes, bash network traffic is filtered by the mode's domain allowlist (package registries + GitHub by default). This is no longer a silent wall - a request to a host outside the allowlist pauses while you're asked (Allow for session / Allow forever / Deny), then proceeds or fails:
alt+ntoggles filtering for the session:Network: filtered(green) ⇄Network: open(orange) in the footer, which always shows the shortcut./net-status(allowlist, session grants/denies),allow <domain…>(grant for the session),open/restrict(same asalt+n),reset(forget session grants/denies).- The model has a
request_network_accesstool: it names the domains and a reason, you approve or deny - one prompt can cover several domains (e.g. all hosts an install needs). Denied hosts stay denied for the session (no prompt-storms from retrying installers), and any blocked host is reported in the command output so the model knows exactly what happened instead of guessing at proxies. - "Allow forever" persists the domain to the active mode's allowlist in your
global config. Set a mode's
sandbox.askOnBlockedHosttofalseto restore silent denying.
Grants, denies, and the open toggle are session-scoped (except "Allow forever") and apply instantly - no sandbox restart.
CLI flags
--perm <mode>- start in a given mode (any defined mode name).--no-sandbox- disable the OS sandbox for the sandboxed modes (falls back to AST/heuristic gating + prompts).
Session approvals
When a mode asks, the prompt offers Allow once / Allow for session / Deny.
"Allow for session" remembers that action (per mode) so it isn't re-asked for the
rest of the session. For bash, the grant is keyed on the command names
extracted from the chain, and every name must already be granted for a
command to pass silently - approving git does not cover a later
git status && curl … | sh (that chain prompts again, and approving it grants
git and curl). Clear them with /perm clear-approvals.
Deny and block. A prompt for an out-of-project path (bash or a file
tool) offers a fourth option, Deny and block <path> for this session.
Plain Deny refuses that one command; the agent is free to try again another
way, and reads outside the project are not contained by the sandbox (see
SECURITY.md), so a script that opens the file would succeed.
Deny and block adds the path to the session's block list, and every
sandboxed command from then on carries it as extra denyRead, so any
indirect read from bash gets nothing (Linux masks a file with /dev/null,
macOS returns a permission error), and the file tools refuse it without a
prompt. The awareness section lists the block, so
the model stops probing. Only the exact file or directory named is blocked,
never the home directory, a system root, or an ancestor of the project or of
a writable root; when a path can't be blocked safely the option is simply not
offered. /perm blocks lists the session's blocks, /perm unblock <path>
lifts one, and /perm clear-approvals clears them with the grants.
How protection works
Two independent layers compose:
- Policy engine (
allow/ask/deny): every tool call resolves against the active mode's policy for its surface (bash,read/write/edit/…, the cross-cuttingpathgate,external_directory,web_search,tool,skill).denyblocks,askprompts,allowpasses. For bash, the command line is parsed with tree-sitter (a real AST) and every command it contains - including ones nested in$(...), backticks, subshells, andsh|bash|… -c '…'scripts (re-parsed recursively) - is judged separately: matched against thebashpatterns and thepathgate (against the joined command and each individual token, so"path": { "*.env": "deny" }blockscat .envwherever the target appears), with out-of-project paths and privilege escalation (even through wrappers likeenv/nice/xargs) detected structurally. If the tree-sitter grammar can't load, a whole-string token-scan heuristic stands in (see SECURITY.md for its limits). - OS sandbox (
@anthropic-ai/sandbox-runtime): the real enforcement for bash. When a mode'ssandbox.enabledis true, in-project bash runs wrapped bysandbox-exec(macOS) /bubblewrap(Linux), confining writes to the profile'sallowWrite(project +/tmp/pi+ the session's scratch directory by default) and denying reads of the profile'sdenyReadsecrets - regardless of what the command does. A mode withsandbox.writable:false(Plan) runs bash read-only;sandbox.enabled:false(YOLO) runs it unsandboxed.
So a bash command that policy says allow still runs sandboxed; ask
prompts, then runs sandboxed - or unsandboxed for an out-of-project escape
or privilege escalation you approve, since you authorized it; deny never runs.
Note: only the model's
bashtool is OS-sandboxed. File tools (read/edit/write/…) are governed by the policy engine and the path checks, not bubblewrap - which is why the Plan-mode Markdown-only rule and the protected-path backstop are enforced at the tool layer. Reads inside the sandbox stay broad (onlydenyReadsecrets are blocked at the kernel), and a non-sandboxing mode (YOLO) confines nothing. SECURITY.md has the full threat model.
Sandbox awareness. While a sandboxed mode is active, a factual
## Sandbox & permissions section is injected into the system prompt each turn:
the writable paths, denied reads, and network allowlist of the merged profile
(project overlays included), plus how the prompt flow works. The model then picks
paths and domains that actually work - project-local installs instead of ~/.npm,
allowlisted hosts instead of dead fetches - and knows a boundary-crossing command
is fine to issue because you'll simply be asked. When the sandbox is degraded, the
section says so and points at the confirmation prompts instead. Opt a mode out
with "injectSandboxInfo": false; unsandboxed modes (YOLO) never inject.
Tool hiding. A mode's hideTools list removes those tools from the model
before it reasons (via the active-tools allowlist), so it never attempts them.
It only ever removes: tools you have off yourself (defaultTools in
settings.json, --exclude-tools) stay off, and switching modes restores
exactly what the previous mode hid. The list is honored literally, including
this extension's own show_plan - a setup that never plans can drop it; a
mode with the "@plan" prompt that hides it gets a warning at load, since
that prompt tells the model to call it.
Skills & extension tools. Skills are gated by name at /skill:<name> (the
skill surface); any non-builtin/extension tool (including MCP-as-tool in a host
that adds them) is gated by tool name (the tool surface). In Default and
Plan, a tool/skill the host adds that has no rule yet prompts on first use -
Allow once / Allow this session / Allow forever / Deny. "Allow forever"
persists <mode>.permission.<tool|skill>.<name>: "allow" to your global config so
it never asks again. Build and YOLO allow all tools/skills without
prompting. (Set a mode's tool/skill to "allow" to opt that mode out of the
first-use prompt, or to "deny" to block.)
Protected paths. Unless a mode sets bypassProtectedPaths (only YOLO does),
edit/write to protected paths are hard-blocked - matched by path segment, not
loose substring, on both the literal target and its symlink-resolved
canonical path (so a link pointing at .git/ or a dotfile can't smuggle a write
past the backstop). The set mirrors the sandbox-runtime mandatory-deny list: .git/,
node_modules/, .vscode/, .idea/, .env/.env.*, .claude/{commands,agents}/,
and common dotfiles (.bashrc, .zshrc, .profile, .gitconfig, .npmrc, …).
This matters most in Build, where file tools aren't OS-sandboxed.
Configuration
Modes are data, layered in this order:
- Stock defaults -
permission-mode.defaults.json, shipped with the extension (the four built-in modes). This is the same format you edit; copy it to make your own, or run/perm initto drop a copy at the global path below. ~/.pi/agent/permission-mode/permission-mode.json(global, full authority): redefine built-in modes, add your own, and setdefaultMode/cycleOrder. (Stable location, independent of wherepi installplaced the extension.)<project>/.pi/permission-mode.json(project, tighten-only): may only make an existing mode stricter. Its permission policy is applied as a most-restrictive overlay (so it can onlyask/denymore, never loosen - no matter what patterns it uses), and its sandbox is intersected/unioned the stricter way. A project config cannot add modes, change defaults, or widen anything. Opening an untrusted repo can never weaken your protection.
Shape
{
"$schema": "https://raw.githubusercontent.com/wynainfo/pi-permission-modes/main/schemas/permission-mode.schema.json",
"defaultMode": "default",
"cycleOrder": ["default", "plan", "build", "yolo"],
"modes": {
"default": { // values here are illustrative - run /perm init for the real defaults
"label": "Default",
"color": "muted", // muted | mdLink | accent | error
"systemPrompt": "@plan", // optional; "@plan" = the dated Plan-mode prompt
"injectSandboxInfo": true, // inject the mode's sandbox boundaries into the system prompt (default true)
"sandbox": {
"enabled": true, // false = run bash unsandboxed (YOLO-style)
"writable": true, // false = bash runs read-only (Plan-style)
"allowWrite": [".", "/tmp/pi"],
"denyRead": ["~/.ssh", "~/.aws", "~/.gnupg"],
"allowRead": [], // readable again inside denyRead (see "Strict home" below)
"denyWrite": [],
"network": { "allowedDomains": ["github.com", "*.github.com"], "deniedDomains": [] },
"askOnBlockedHost": true // live prompt for hosts outside the allowlist (false = silent deny)
},
"permission": {
"path": { "*": "allow", "*.env": "deny" }, // cross-cutting gate (deny overrides per-tool allow)
"external_directory": "ask", // the out-of-project boundary
"read": "allow", "grep": "allow", "find": "allow", "ls": "allow",
"write": "ask", "edit": "ask",
"bash": { "*": "ask", "sudo*": "deny" }, // matched per command (name + args)
"web_search": "ask",
"tool": "allow", // any non-builtin/extension tool, by name
"skill": "allow" // skills, by name
},
"hideTools": [] // tools removed from the model in this mode
}
}
}
Actions: allow (pass through), ask (prompt), deny (block). A surface is
either a single action or a { "<glob>": <action> } map where * matches any
characters incl. /, ? matches one, ~/$HOME expand, the last matching
pattern wins (put "*" first as the default), and across the path /
external_directory / per-surface layers the most restrictive wins.
Keeping your global config current
/perm init writes a full copy of the stock defaults so you can edit values
in place. Every value in that file overrides the shipped default, which means a
later change to the defaults (2.3.0 narrowed allowWrite from /tmp to
/tmp/pi, for example) does not reach a copy that still carries the old value.
Two safeguards cover that:
- Outdated-default warnings, every session start. Each value in your global
file is compared with the current default and with every older default
shipped since 2.0.0 (
defaults-history.json, clear-text copies with their version ranges). A value equal to the current default is fine; one that differs from it but equals an older default is reported by field, with both values and the versions it belonged to; one that equals no default is your customization and stays silent. Custom modes never trigger it. - A one-time notice after an upgrade. When the extension version changes
and the stock defaults changed between the two versions, users with a global
config are told which fields changed, so a heavily customized file gets a
nudge to compare even when nothing in it matches an old default verbatim.
The last version seen is kept in
~/.pi/agent/permission-mode/state.json.
To keep an outdated value knowingly, set "acknowledgeDefaults": "<version>"
(the version whose defaults you reviewed against; /perm init stamps the copy's
origin in a $comment for reference). The warnings stay silent until the
shipped defaults change again. Deleting values you never meant to change is the
simpler fix: the file is an overlay, and whatever it omits follows the defaults.
Defining your own mode
Add a mode under modes in the global config and (optionally) list it in
cycleOrder. Example - a "review" mode: read-only sandbox, web search off, the
edit/write tools hidden entirely:
{
"cycleOrder": ["default", "review", "build", "yolo"],
"modes": {
"review": {
"label": "Review", "color": "mdLink",
"sandbox": { "enabled": true, "writable": false, "allowWrite": [".", "/tmp/pi"], "denyRead": ["~/.ssh"] },
"permission": { "read": "allow", "bash": "allow", "web_search": "deny", "write": "deny", "edit": "deny" },
"hideTools": ["edit", "write"]
}
}
}
Linux glob limitation (sandbox filesystem only). The sandbox-runtime drops glob patterns from its
allowWrite/denyRead/denyWritelists on Linux - use literal paths there (macOS supports globs). This applies to the sandbox lists, not thepermissionpolicy globs, which are matched by this extension.
Strict home (optional). Reads outside the project are deny-listed, not allow-listed: the sandbox blocks only
denyRead(credential files and dirs by default, see the defaults file) and lets everything else under~be read so toolchains work.allowReadturns that around for a mode: deny the whole home directory, then re-open only what the toolchain needs (deny-then-allow; a more specificdenyReador a session block inside a carve-out still wins). Paste into a mode'ssandboxand trim the list:"denyRead": ["~"], "allowRead": [".", "/tmp/pi", "~/.gitconfig", "~/.cache", "~/.config", "~/.local", "~/.npm", "~/.nvm", "~/.cargo", "~/.rustup", "~/go", "~/.pyenv", "~/.gem"]Keep
.in the list: a read-only mode (Plan) does not re-expose the project throughallowWrite, and without it the project itself reads as empty. Keep~/.gitconfigtoo, or everygit commitinside the sandbox fails with "Author identity unknown" (the sandbox reports the file as absent, not as denied, so nothing else tells you why). The extension re-opens its own runtime directory automatically (the sandbox's seccomp helper lives there, under~/.pi/agent/...); the interpreter that runs pi does not need to be listed either, only what the agent's commands execute (~/.nvm,~/.cargo, a venv outside the project). Everything under~that is not listed reads as absent inside bash (Linux mounts an empty directory over it; no error, no prompt), so a tool that needs a home path you left out fails silently;/sandboxshows the lists, and the awareness section tells the model what is masked.~/.configand~/.localare where most CLIs keep both their settings and their tokens (gh, many others); leave them out only if the agent never needs those tools. A project config may remove carve-outs, never add them. Note that~/.pi/agent/auth.jsonin the defaultdenyReadmeans apistarted from inside sandboxed bash cannot authenticate; remove it if you spawn nested pi sessions that way.
Temp directories, per platform. The runtime always allows writes to its own
/tmp/claude, regardless ofallowWrite; on macOS/tmpis/private/tmpand both spellings are handled. The per-user macOS temp dir under/var/folders/…is not writable (it was with runtime 0.0.26);TMPDIRinside sandboxed bash points at the session scratch directory instead. Windows has no OS sandbox, so thereallowWriteonly feeds the prompt bounds. A tool that hardcodes/tmpand ignoresTMPDIRfails silently inside the sandbox (no prompt - the kernel denies it); add/tmpback to the mode'sallowWriteif you need such a tool, at the cost of sharing/tmpwith everything else on the host (and setacknowledgeDefaultsso the outdated-default warning stays quiet, see below).
Disabling
- Temporarily: launch pi with
--no-extensions(disables all), or--no-sandboxto keep the modes but drop OS sandboxing. - Permanently:
pi remove npm:pi-permission-modes(orgit:github.com/wynainfo/pi-permission-modes, matching how you installed it) - or remove the entry from your pi settings.