@jocax/security-guard0.2.0
Rule-based guard for pi tool calls: built-in rules for dangerous commands and sensitive files, YAML path tiers and bash patterns, project scope, and an audit log
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
@jocax/security-guard
Rule-based guard for pi tool calls. It blocks dangerous commands, protects sensitive files, asks before a tool reaches outside the project, and applies the rules you write per project.
What
The extension checks every tool call before it runs and decides once: allow, ask you, or block.
Rules come from three places and apply together. The strictest result wins.
| Source | Where | Active |
|---|---|---|
| Built-in | the package | always |
| Global file | ~/.pi/security-guard.yaml |
when the file exists |
| Project file | .pi/security-guard.yaml |
when the file exists |
A rules file can add protection. It cannot remove a built-in rule or a rule of the other file.
How a call is decided
bash:
- A pattern that blocks matches: blocked.
- The command starts with
rmormvand an argument is a no-delete path: blocked. - A pattern that asks matches: dialog.
- Otherwise allowed.
read, write, edit:
- Zero-access path: blocked.
- Read-only path, for
writeandedit: blocked. - Confirm path: dialog.
readof a trusted read root: allowed.- Path outside the project: dialog.
- Otherwise allowed.
grep, find, ls: allowed in a trusted read root, dialog outside the project, otherwise allowed.
The path tiers are not checked for these three tools.
A call the rules allow can still go to the model stage, which is off unless switched on.
Why
A pi agent has full system access. One bad prompt or one wrong command can delete a home directory, overwrite a file of secrets, or pipe a download into a shell. The guard stops the known cases, asks about the doubtful ones, and leaves ordinary work inside the project alone.
Install
Install the package from npm:
{
"source": "npm:@jocax/security-guard"
}
Or try it from a local checkout without installing:
pi -e ./src/index.ts
Usage
The guard loads at pi startup. The footer shows 🛡️ Guard: <n> rules, followed by
+ model (<model>) when the model stage is on.
Command
| Command | Description |
|---|---|
/security-guard |
Mode, rule counts, the model stage, and the audit totals with the five most recent entries |
Built-in rules
Blocked, 18 patterns:
| Group | Example |
|---|---|
Recursive force-delete of /, the home directory or a system directory |
rm -rf / |
| Writes to a block device, filesystem and swap creation | dd if=… of=/dev/sda, mkfs.ext4 … |
Fork bomb, recursive chmod 777 or chown on / |
chmod -R 777 / |
| Download piped into a shell | curl https://example.com/x.sh | bash |
Dialog, 16 patterns:
| Group | Example |
|---|---|
Recursive delete, rmdir |
rm -rf node_modules |
sudo, stopping a service, shutdown, reboot |
sudo systemctl stop nginx |
chmod 777, recursive chmod or chown |
chmod -R 755 dir |
Force push, hard reset, git clean |
git push --force origin main |
curl piped to a command, wget -O |
curl https://example.com | jq . |
Dialog on read, write and edit, 16 path globs: environment files, credentials.*, *.pem,
*.key, SSH private keys, .git/config, .ssh/config, .aws/credentials, .npmrc,
secrets.yml, secrets.yaml, .secrets.
Project scope
read, write, edit, grep, find and ls run without a dialog inside the project,
the directory pi was started in. A path outside the project opens a dialog.
Pi's own sources can be read without a dialog: the pi SDK directory, ~/.pi/ and .pi/.
This covers read, grep, find and ls. Writing there still asks when the path is
outside the project.
Model stage
An optional second stage asks a decision model about the calls the rules allow. It is off
unless AE_PI_SECURITY_GUARD_MODEL_ENABLED=true and needs a decision provider configured
through @jocax/decision-core, see its configuration.
A call the rules block or ask about never reaches the model.
| Check | Runs on | Asks | Outcome |
|---|---|---|---|
bash |
a bash call the rules allow |
what the command does to the machine, and whether it aims to destroy | allow, dialog, block |
write |
a write or edit call the rules allow |
what kind of file it is, and whether the content holds a real credential | allow, dialog, block |
screen |
a read or bash result |
whether the output carries instructions aimed at the agent | a warning banner above the output |
- The questions and thresholds live in
src/model-questions.ts. A rules file or variable cannot change them. - Model unreachable or failing: a
bashorwritecheck opens aModel Unavailabledialog; a screened result stays unchanged and is recorded asmodel-error. - Switched on but not configured: a warning at session start, and the rules still apply.
- Cost: with all three checks on, the guard adds one model call per allowed
bash,writeandeditcall and one perreadandbashresult.AE_PI_SECURITY_GUARD_MODEL_STAGES=bash writeturns off the screen check, the most frequent one.
Audit log
Every block, every confirmed dialog and every denied dialog is recorded in the session.
A flagged result and a result the model could not screen are recorded too.
/security-guard shows the totals and the five most recent entries; the flagged and
unscreened totals appear on a second line when either is non-zero.
With AE_PI_EVENT_CORE_ENABLED=true every audit entry is also reported on the package
event channel (@jocax/event-core), with the tool call it belongs to. The demo kit turns
this on; /security-guard counts only audit entries.
Configuration
Rules file
Create .pi/security-guard.yaml in the project or ~/.pi/security-guard.yaml for all
projects. Start from the example:
cp software/packages/security-guard/assets/example-rules.yaml .pi/security-guard.yaml
The guard reads the files when a session starts.
paths:
# read, write and edit blocked
zero-access:
- "**/secrets/**"
# write and edit blocked
read-only:
- "**/node_modules/**"
# rm and mv blocked
no-delete:
- "**/*.md"
# read, write and edit ask
confirm:
- "**/*.secret"
bash:
patterns:
# ask: true opens a dialog
- regex: "npm\\s+(uninstall|remove|rm)"
ask: true
label: "npm uninstall"
# no ask: blocked
- regex: "npm\\s+publish"
label: "npm publish"
The parser reads a subset of YAML: two-space indentation, lists, quoted and unquoted
strings, true and false, and comments on their own line. A file that fails to parse
is ignored with a warning.
Glob syntax. A glob is tested against the absolute path.
*matches within one path segment**/matches zero or more directories- a leading
~is the home directory
Environment variables
| Variable | Values | Default | Description |
|---|---|---|---|
AE_PI_SECURITY_GUARD_ENABLED |
false |
unset | false turns the guard off |
AE_PI_SECURITY_GUARD_MODE |
block-abort, block-continue |
block-abort |
Wording of the reason the model receives for a blocked call |
AE_PI_SECURITY_GUARD_ALLOW_COMMANDS |
space-separated prefixes | unset | A command that starts with a prefix skips the built-in bash patterns and the model bash check. Patterns from rules files and no-delete paths still apply |
AE_PI_SECURITY_GUARD_TRUSTED_READ_PATHS |
space-separated directories | unset | Extra directories that may be read outside the project without a dialog |
AE_PI_SECURITY_GUARD_MODEL_ENABLED |
true |
unset | true turns the model stage on; any other value leaves it off |
AE_PI_SECURITY_GUARD_MODEL_STAGES |
space-separated subset of bash write screen |
all three | Checks that run. Unknown names are dropped with a warning |
AE_PI_SECURITY_GUARD_MODEL_HEADLESS |
block, allow |
block |
What a model dialog does when there is no UI |
AE_PI_SECURITY_GUARD_MODEL_TIMEOUT_MS |
positive integer of ms | 5000 |
Budget for one model call. Overrides AE_PI_DECISION_CORE_TIMEOUT_MS |
AE_PI_EVENT_CORE_ENABLED |
true |
unset | true also reports every audit entry as a package event (see @jocax/event-core) |
A rules file cannot loosen the guard. The only settings that do are
AE_PI_SECURITY_GUARD_ENABLED=false, AE_PI_SECURITY_GUARD_ALLOW_COMMANDS,
AE_PI_SECURITY_GUARD_TRUSTED_READ_PATHS and AE_PI_SECURITY_GUARD_MODEL_HEADLESS=allow.
Modes
block-abort: the model receives a short reason,🛡️ Security Guard BLOCKED: <rule>.block-continue: the reason also tells the model to avoid the operation, find another approach and continue with other tasks.
Print mode
pi -p has no dialog. Every call the rules would ask about is denied and recorded as denied.
A model stage dialog blocks instead, unless AE_PI_SECURITY_GUARD_MODEL_HEADLESS=allow
lets the call through.
Examples
# Built-in rules
pi -e software/packages/security-guard/examples/basic.ts
# With a rules file
pi -e software/packages/security-guard/examples/custom-rules.ts
# With the model stage (export the variables listed in the file first)
pi -e software/packages/security-guard/examples/model-stage.ts
Run the demo
just demo-web security-guard # browser
just demo security-guard # herdr
The sandbox copy gets a .env, a config/credentials.json and a
.pi/security-guard.yaml adding one zero-access and one read-only rule
on top of the always-active built-ins. The decision-model stage stays off.
Every scenario is two turns, one guarded call each. pi runs the tool calls of
one assistant message as a parallel batch and holds each call's result until
the batch's last dialog is answered, so one dialog at a time keeps the panel
readable. In the hook panel, a dialog's row shows only its title and the
answer (→ yes / → no) — the browser collapses the message's line breaks.
Scenario "Secrets ask, then stay out of reach"
| Step | Do this | Watch |
|---|---|---|
| 1 | Send the first prompt | A confirm dialog titled 🛡️ Security Guard — Sensitive File names Environment file (.env) |
| 2 | Allow it | The row turns ran; under it a security-guard confirmed read ".env" event |
| 3 | Send the second prompt | No dialog. The call fails with 🛡️ Security Guard BLOCKED: zero-access path: **/config/credentials.json, and the row carries a blocked event |
| 4 | Type /security-guard |
Mode: block-abort, rule counts per tier, and an audit tail with both decisions |
Scenario "Destructive command"
| Step | Do this | Watch |
|---|---|---|
| 1 | Send the first prompt | A confirm dialog titled 🛡️ Security Guard names Force-delete directory (rm -rf) and shows Command: rm -rf build |
| 2 | Deny it | Blocked with … Force-delete directory (rm -rf) (denied by user), and the audit records denied |
| 3 | Send the second prompt | A confirm dialog names Hard reset (git reset --hard) — a built-in rule, not git-workflow's |
| 4 | Allow it | The reset runs and the audit records confirmed |
Scenario "Writing outside the project"
| Step | Do this | Watch |
|---|---|---|
| 1 | Send the first prompt | A confirm dialog titled 🛡️ Security Guard — Outside Project names the file and the sandbox directory it escapes |
| 2 | Deny it | Blocked with … outside project (denied by user), and the audit records the scope tier |
| 3 | Send the second prompt | The same write inside the project runs with no dialog |
Every decision also arrives on the package event channel with the tool call's id, which is why the browser shows it under that call's row rather than only in the timeline. Reloading the page while a dialog is open restores the timeline and reopens the dialog.
Needs herdr 0.9.3 or newer and one reachable model provider — see demo-kit.
Development
npx vitest run software/packages/security-guard
npx tsc --noEmit -p software/packages/security-guard/tsconfig.json
# Live model stage suites, opt-in
AE_PI_SECURITY_GUARD_MODEL_LIVE=ollama npx vitest run software/packages/security-guard/tests/model-live.test.ts
AE_PI_SECURITY_GUARD_MODEL_LIVE=typesafe npx vitest run software/packages/security-guard/tests/model-live.test.ts