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

Packages

Package details

extensionskill

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-exec on macOS, bubblewrap on 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. grep output 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-exec is 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 untrust removes the trust. /security shows 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 release
  • feat: ... → minor release
  • feat!: ... or a BREAKING CHANGE: footer → major release
  • chore:, 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:

  1. Determines the next version from commits since the last release.
  2. Generates release notes and prepends them to CHANGELOG.md.
  3. Publishes to npm (npm publish --provenance) and bumps package.json.
  4. Creates the vX.Y.Z git tag and GitHub release.
  5. Commits CHANGELOG.md/package.json back to main (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:

  1. Go to the package's Settings → Trusted Publisher on npmjs.com.
  2. Select GitHub Actions and configure: organization/user hannesro, repository pi-secure-it, workflow filename release.yml, allowed action npm publish.
  3. (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