pi-secure-it
In-process tool guard and subagent posture hardening for Pi coding agent. Enforces filesystem allow/deny lists and domain allowlists on read, grep, find, ls, write, edit, fetch_content, web_search, and get_search_content — with interactive ask-tier prompt
Package details
Install pi-secure-it from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-secure-it- Package
pi-secure-it- Version
2.0.0- Published
- Oct 2, 2026
- Downloads
- 397/mo · 364/wk
- Author
- hannesro
- License
- MIT
- Types
- extension, skill
- Size
- 1.6 MB
- Dependencies
- 1 dependency · 1 peer
Pi manifest JSON
{
"image": "https://raw.githubusercontent.com/hannesro/pi-secure-it/main/banner.png",
"skills": [
"./skills"
],
"extensions": [
"./security-guard.ts",
"./sandbox/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-secure-it
Two-layer security extension for the Pi coding agent:
- Layer 1 — OS-level bash sandbox (
sandbox-execon macOS,bubblewrapon Linux) that blocks filesystem writes outside an allow-list, reads of sensitive paths, and network egress to unlisted domains. - Layer 2 — In-process tool guard applying the same policy to the tools the OS sandbox can't reach:
read,grep,find,ls,write,edit,fetch_content,web_search,get_search_content.grepoutput lines from denied files beneath an allowed search root are removed before the model sees them. - Layer 3 — Subagent posture: optionally drop or restrict network access when running headless (
-p, JSON mode, subagents).
When a tool call is blocked you get an interactive prompt — no need to leave pi and hand-edit config files. Choose this once, always for this project, or always for all projects (file or parent-folder granularity). Decisions are persisted and audited.
Install
# From npm (once published)
pi install npm:pi-secure-it
# From git
pi install git:github.com/hannesro/pi-secure-it
# Try without installing
pi -e git:github.com/hannesro/pi-secure-it
Requirements
- macOS or Linux
- macOS:
sandbox-execis built in - Linux:
bubblewrap,socat
Configuration
Policy files are merged in order:
| File | Scope |
|---|---|
~/.pi/agent/extensions/sandbox.json |
Global (all projects) |
<cwd>/.pi/sandbox.json |
Project-local (auto-written by ask-tier prompts) |
Copy sandbox.example.json from this package as a starting point for your global config.
Key fields
{
"enabled": true, // set false to disable Layer 2 without --yolo
"network": {
"allowedDomains": [ // domains fetch_content / get_search_content may reach
"github.com", "*.github.com",
"registry.npmjs.org"
],
"deniedDomains": [] // explicit block-list (checked before allowedDomains)
},
"filesystem": {
"denyRead": ["~/.ssh", "~/.aws"], // Layer 1 + Layer 2: no read at all
"modelDenyRead": ["~/.netrc"], // Layer 2 only: model's read tool blocked; subprocesses ok
"allowWrite": [".", "/tmp"], // Layer 2: writes only inside these roots
"denyWrite": [".env", "*.pem"] // Layer 1 + Layer 2: write always blocked
},
"subagent": {
"network": "allow" // "allow" | "deny" | "research-only"
}
}
Project policy files and trust
A folder can have its own .pi/sandbox.json. It comes with the folder, for example with a repository that you clone. So pi-secure-it does not trust it automatically (ADR-013).
- Block rules in the file always apply. They are added to your rules.
- Changes that make your security weaker do not apply until you trust the file. Examples: turn off pi-secure-it, let bash write to
/, allow more websites. - pi-secure-it shows a warning that lists these changes in plain words.
- Did you write the file? Type
/security trust. pi-secure-it shows the changes and asks you. If you say "No", it does not warn again until the file changes. - Trust is for the file as it is now. If the file changes, pi-secure-it asks again.
- Most folders have no
.pi/sandbox.json, so you see nothing. Your own "always for CURRENT project" answers keep the file trusted. /security untrustremoves the trust./securityshows the status.
Outside the project
Reads outside the working directory can ask before they run (ADR-012):
"filesystem": {
"outsideProject": {
"read": "ask", // "allow" (default) | "ask" | "deny"
"allowRead": ["~/repos", "~/.cargo/registry"]
}
}
Never asked about: the project itself, your allowWrite roots, ~/.pi/agent (skills, settings; auth.json stays absolute-denied), pi's own package (docs, examples) and allowRead. In an interactive session a read runs without a prompt when you named its full path (/… or ~/…) in one of your own messages; only messages you typed count, never tool output or the agent's text. Otherwise you get the ask-tier prompt, including yes — for this session. Headless runs block (set allowRead for paths a harness needs). Writes outside the project are governed by allowWrite as before.
Ask-tier prompt
no — block is pre-selected, so Enter alone blocks. Besides yes — this once and the persistent always options there is yes — for this session (this file, its folder, or this domain), kept in memory until the session ends and never saved (ADR-010).
File-name patterns
An entry without / or ~ (.env, *.key) is a file name. pi's own tools (Layer 2) match it anywhere on disk; bash (Layer 1) matches it anywhere under the project directory, so packages/api/.env is covered too. Use a full path (~/other/.env) to cover a file outside the project for bash.
Absolute-deny tier
Access to ~/.ssh, ~/.gnupg, ~/.aws, *.pem, *.key and pi's own ~/.pi/agent/auth.json is always a high-risk block. Allowing one call takes two menus, each with "block" pre-selected: pick allow this ONE call, then confirm with Yes. Pressing Enter twice blocks. The "always" option is never offered (ADR-009).
A policy file that does not parse
sandbox.json is strict JSON: no comments, no trailing commas. If a file does not parse, both layers skip it and use the built-in defaults for it. Layer 2 then shows an error at session start and writes policy-parse-error to the audit log. The "always" prompts refuse to write to a file they cannot parse, so a hand-written policy is never replaced.
Commands
| Command | Description |
|---|---|
/security |
Show Layer 2 policy, project-local overrides, last 10 audit events |
/sandbox |
Show Layer 1 (bash sandbox) config |
/sandbox reload |
Live-reload sandbox after manual edits to sandbox.json |
Escape hatches
pi --yolo # disables ALL layers globally (visible warning banner)
pi --no-sandbox # alias for --yolo
Audit log
Every block/allow/always decision is appended to ~/.pi/agent/audit.log as a JSON line:
{
"ts": "2026-07-15T10:00:00.000Z",
"layer": 2,
"tool": "write",
"subject": "/Users/you/project/secret.pem",
"reason": "denyWrite matched \"*.pem\"",
"decision": "no",
"cwd": "/Users/you/project"
}
Development
git clone https://github.com/hannesro/pi-secure-it
cd pi-secure-it
npm install # installs typescript for typecheck
npm run typecheck # type-checks against pi's bundled .d.ts files
pi -e . # load extension for the current session only
Releasing
Releases are fully automated with semantic-release, driven by Conventional Commits on main:
fix: ...→ patch releasefeat: ...→ minor releasefeat!: ...or aBREAKING CHANGE:footer → major releasechore:,docs:,refactor:,test:,ci:etc. → no release by themselves
Commit messages on pull requests are checked by commitlint (.github/workflows/commitlint.yml). On every push to main, .github/workflows/release.yml runs semantic-release, which:
- Determines the next version from commits since the last release.
- Generates release notes and prepends them to
CHANGELOG.md. - Publishes to npm (
npm publish --provenance) and bumpspackage.json. - Creates the
vX.Y.Zgit tag and GitHub release. - Commits
CHANGELOG.md/package.jsonback tomain(chore(release): ... [skip ci]).
Nothing to run locally beyond writing conventional commit messages — just merge to main. Publishing uses npm Trusted Publishing (OIDC) — no NPM_TOKEN secret required. One-time setup on npmjs.com:
- Go to the package's Settings → Trusted Publisher on npmjs.com.
- Select GitHub Actions and configure: organization/user
hannesro, repositorypi-secure-it, workflow filenamerelease.yml, allowed actionnpm publish. - (Recommended) Under Settings → Publishing access, choose "Require two-factor authentication and disallow tokens" to disable classic token-based publishing entirely, and revoke any automation tokens you previously created.
GITHUB_TOKEN is provided automatically by Actions; the id-token: write permission in release.yml is what lets npm's OIDC exchange work.
License
MIT
