Pi
Minimal, extensible TypeScript coding agent with pinned interpreter/package verification, external sandbox policy, and local-provider side profiles.
Last updated
Cloud API Local Provider TypeScript1. Installation
Prerequisites
- macOS 13+ or Linux (kernel 5.13+)
- Node.js 20.6+
- API key for at least one cloud provider, or a local OpenAI-compatible endpoint
Install Pi
npm install -g @mariozechner/pi-coding-agent
Install the preferred stack
brew tap nvk/tap
brew install nvk/tap/agent-bondage
brew install nvk/tap/envchain-xtra
brew install nono
Verify
pi --version
bondage --help
nono --version
envchain --version
2. nono Profile
Pi is structurally similar to Claude Code — a Node.js agent with bash, read, write, and edit tools. In the preferred setup, bondage verifies both the Pi entrypoint and the Node interpreter before handing execution to nono.
What the sandbox allows
| Resource | Access | Why |
|---|---|---|
| Current working directory | Read + Write | Project files |
~/.pi/agent/ | Read + Write | Config, sessions, extensions |
| Provider API endpoints | Network | API calls |
/dev/tty, /dev/null | Read + Write | Terminal I/O |
/dev/urandom | Read | Crypto randomness |
/tmp/ | Read + Write | Bash output temp files |
3. envchain-xtra
Pi supports many providers. Store keys in one envchain-xtra namespace and let Pi read them from the environment automatically — no plaintext config file needed.
Store your API key
# For Anthropic (most common)
envchain --set pi ANTHROPIC_API_KEY
# Add more providers to the same namespace
envchain --set pi OPENAI_API_KEY
envchain --set pi GEMINI_API_KEY
Key provider env vars
| Provider | Env Var |
|---|---|
| Anthropic | ANTHROPIC_API_KEY |
| OpenAI | OPENAI_API_KEY |
| Google Gemini | GEMINI_API_KEY |
| DeepSeek | DEEPSEEK_API_KEY |
| Groq | GROQ_API_KEY |
| xAI | XAI_API_KEY |
| OpenRouter | OPENROUTER_API_KEY |
--api-key flag, then auth.json, then environment variable. With envchain, skip auth.json entirely — no plaintext keys on disk.
4. bondage Wrapper
Add this thin wrapper to your shell config:
pi() {
bondage exec pi ~/.config/bondage/bondage.conf -- "$@"
}
Sample stack snippets
Assuming your shared [global] block already exists in ~/.config/bondage/bondage.conf, this is a script-backed Pi shape to adapt:
# ~/.config/bondage/bondage.conf
[profile "pi"]
namespace = pi
use_envchain = true
use_nono = true
nono_profile = pi
touch_policy = prompt
target_kind = script
target = /absolute/path/to/pi/dist/cli.js
target_fp = sha256:replace-me
interpreter = /absolute/path/to/node
interpreter_fp = sha256:replace-me
package_root = /absolute/path/to/pi
package_tree_fp = sha256:replace-me
nono_allow_cwd = true
nono_allow_file = /dev/tty
nono_allow_file = /dev/null
nono_read_file = /dev/urandom
ensure_dir = /Users/you/.pi
{
"extends": "default",
"meta": {
"name": "pi",
"description": "Pi with envchain-backed provider keys and writable local state"
},
"policy": {
"add_deny_access": ["/Volumes"],
"add_allow_readwrite": [
"$HOME/.pi"
]
},
"workdir": {
"access": "readwrite"
}
}
How it works
- The shell wrapper passes only the profile name and your arguments
bondageverifies the exact Pi entrypoint, Node runtime, and package treeenvchain-xtrainjects the selected provider keysnonoapplies the filesystem and network policy
Reduce startup network
# Add to ~/.zshrc
export PI_OFFLINE=1
export PI_SKIP_VERSION_CHECK=1
The current package scan found offline/version-check controls, not a dedicated PI_TELEMETRY switch. Provider calls still follow the model endpoint you select.
Reload your shell:
source ~/.zshrc
5. Local pi-ds4 Profile
For local ds4, use a separate Pi agent directory and a separate bondage profile. The goal is to make pi-ds4 disposable without changing the normal pi profile, default provider selection, or cloud credentials. Start with a no-tools chat profile; keep tool-use experiments isolated because direct ds4 tool requests can stall the server.
Install the upstream ds4 extension
The default local ds4 path for Pi is now the upstream mitsuhiko/pi-ds4 extension. Install it into the same isolated Pi state directory used by the side profile:
PI_CODING_AGENT_DIR="$HOME/.local/state/agent-stack/pi-ds4" \
pi install https://github.com/mitsuhiko/pi-ds4
PI_CODING_AGENT_DIR="$HOME/.local/state/agent-stack/pi-ds4" \
pi --model ds4/deepseek-v4-flash --thinking off -p 'reply with OK'
mitsuhiko/pi-ds4 register the model and manage ds4-server. It starts the server on demand, chooses q2 on 128GB Macs, keeps leases under ~/.pi/ds4, and exposes /ds4 for logs. Launch the public wrapper with --no-tools until the local tool path passes direct server tests.
Side profile shape
# ~/.config/bondage/bondage.conf
[profile "pi-ds4"]
use_envchain = false
use_nono = true
nono_profile = custom-pi-ds4
target_kind = script
target = /absolute/path/to/pi/dist/cli.js
target_fp = sha256:replace-me
interpreter = /absolute/path/to/node
interpreter_fp = sha256:replace-me
package_root = /absolute/path/to/pi
package_tree_fp = sha256:replace-me
ensure_dir = /Users/you/.pi-ds4
env_set = PI_CODING_AGENT_DIR=/Users/you/.pi-ds4
env_set = SHELL=/bin/zsh
target_arg = --no-tools
target_arg = --model
target_arg = ds4/deepseek-v4-flash
target_arg = --thinking
target_arg = off
This profile assumes the upstream extension has already been installed into PI_CODING_AGENT_DIR. The extension owns model registration and ds4 lifecycle; the bondage profile supplies sandboxing and the model selector. Use Claude/Codex ds4 profiles for local file editing while Pi ds4 tool-use remains experimental.
"Let me" or "The user wants" can make local models return an empty message before they call read or edit. Put behavior rules in the system prompt instead.
Direct benchmark profile
Keep a no-nono profile only for benchmark runs and nested-sandbox environments. That lets you measure Pi plus ds4 without weakening the normal pi-ds4 profile.
[profile "pi-ds4-rawdog"]
inherits = git-env,pi-script-target
use_envchain = false
use_nono = false
touch_policy = none
ensure_dir = /Users/you/.pi-ds4
env_set = PI_CODING_AGENT_DIR=/Users/you/.pi-ds4
env_set = SHELL=/bin/zsh
target_arg = --no-tools
target_arg = --model
target_arg = ds4/deepseek-v4-flash
target_arg = --thinking
target_arg = off
Shell shortcut
pi-ds4-install() {
PI_CODING_AGENT_DIR="$HOME/.local/state/agent-stack/pi-ds4" \
pi install https://github.com/mitsuhiko/pi-ds4
}
pi-ds4() {
bondage exec pi-ds4 ~/.config/bondage/bondage.conf -- "$@"
}
pi-ds4-rawdog() {
bondage exec pi-ds4-rawdog ~/.config/bondage/bondage.conf -- "$@"
}
pi-ds4-bench() {
pi-ds4-rawdog -p "${*:-reply with OK}"
}
Smoke test
pi-ds4 -p 'reply with OK'
pi-ds4-bench
With the current upstream extension, print-mode smoke tests may include DeepSeek-style reasoning text before the final answer even with --thinking off. Count the smoke test as passing when it reaches the local ds4 server and ends with OK. In local testing, direct ds4 requests with OpenAI-style tools stalled before producing a file, so the public wrapper keeps tools disabled by default.
Current upstream smoke benchmark
| Path | Result | Note |
|---|---|---|
pi-ds4-bench no-tools | ~3-4 s | Reached local ds4 and ended with OK |
Direct ds4 tools request | stalled over 30 s | No file produced; do not enable by default |
Legacy manual-profile benchmark
These numbers are from the older manual models.json + custom extension setup. Keep them as a harness-overhead baseline, not as the default upstream-extension recommendation.
| Setting | Pass rate | Average | OK | Read | Edit |
|---|---|---|---|---|---|
| 16k / 2048 | 6/6 | 13.8 s | 8.6 s | 9.7 s | 23.2 s |
| 32k / 2048 | 6/6 | 16.4 s | 9.3 s | 12.0 s | 27.7 s |
| 32k / 4096 | 6/6 | 19.9 s | 11.8 s | 15.5 s | 32.4 s |
| 64k / 4096 | 6/6 | 21.0 s | 13.4 s | 16.7 s | 32.8 s |
6. Verification
Test the full chain
bondage verify pi ~/.config/bondage/bondage.conf
bondage chain pi ~/.config/bondage/bondage.conf -- --help
pi --help
Confirm no plaintext keys
# Should return nothing
grep -r "sk-ant\|sk-proj" ~/.pi/ 2>/dev/null
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "Invalid API key" | envchain namespace or key name wrong | envchain --set pi ANTHROPIC_API_KEY |
| Session data not saved | nono blocking ~/.pi/ |
Add --allow-file ~/.pi/ to wrapper |
| Extensions fail to install | npm cache blocked by sandbox | Add --allow-file ~/.npm/ to wrapper |
| Local ds4 fails near full context | Client advertised the full server window | Use the upstream mitsuhiko/pi-ds4 chat path with --no-tools; test direct server tool requests before enabling Pi file edits |
| Blank response instead of edit | A stop sequence cut off the model before tool use | Remove behavioral stop strings and enforce tool behavior through the system prompt |