Safety guardrails¶
Owners: pi-extensions
(read at 7cb808e), the shared fleet extensions in
docker-compose-repo
common/pi (read at dc3770f), and the baked skills in
pi-devbox
An agent with a shell can do a lot of damage quickly: delete files, overwrite history that others share, or copy a password into a place it cannot be removed from. Plain pi does not ask for approval before a tool call. Its own security guide says safety comes from limiting what pi can reach, and running it inside a container is usually the strongest option. That is what the image does: host files that are not mounted into the container are out of reach.
Inside the container the agent can still reach a lot: the mounted work directories, the credentials it needs, the network and the shared palace. The guardrails on this page cover that space. Each one covers one kind of risk, and they overlap on purpose. None of them is a sandbox. They check what the agent is about to do, and each section says what its check cannot see.
What can go wrong, and what catches it¶
| Risk | Guardrail | Where it acts |
|---|---|---|
| A command destroys data or rewrites shared history | erdc-guard, or confirm-destructive |
before a shell command runs |
| A secret value reaches the model provider or a saved session | secret-guard |
on every tool result and model request |
| A credential is written into the shared memory, where it cannot be taken back | erdc-palace-check |
before a write to the palace |
| A child agent changes more than it was asked to | the task tool and fork-gate |
when work is delegated |
| A secret is committed to git | gitleaks, and encrypted environment files | at commit time and in CI |
| A credential leaks anyway | the credential-incident-response skill |
when a leak is found |
| The agent's edits need to be undone | git-checkpoint |
at the start of every turn |
Asking before destructive commands¶
Two extensions do this job in different ways. Only one of them checks shell commands at a time.
confirm-destructive: fixed patterns¶
confirm-destructive (pi-extensions) matches each shell command against a list
of dangerous patterns: recursive deletes, sudo, git push --force, writes to
disk devices, formatting a filesystem and a few more. On a match it asks the user
and waits for the answer. When nobody can answer, as in a non-interactive
pi -p run, it blocks the command.
It also asks before /new, /resume and /fork throw away or leave the current
session.
Patterns have two weaknesses. They ask about harmless commands that look
dangerous, and they miss dangerous commands that match no pattern. And because
the question has no time limit, a long session that nobody is watching stops at
the first one. Measured on 2026-10-23 over 300 real commands, the patterns would
have asked 17 times, nearly always about something harmless such as deleting a
temporary directory. They caught none of 28 destructive commands outside their
list, such as git reset --hard, DROP TABLE or terraform destroy.
erdc-guard: a classifier judges each statement¶
erdc-guard (fleet extension) replaces the pattern list with a classifier: a
small model that does not chat, but answers one fixed question with a score. Here
the question is whether a shell statement could destroy data or rewrite shared
history. The model is ERDC's clef-flash.
flowchart LR
cmd["Shell command"] --> split["Split into<br/>statements"]
split --> skip["Plainly read-only?<br/>run it"]
split --> score["Classifier<br/>scores the rest"]
score --> ask["High score:<br/>ask the user"]
ask --> run["Yes: run"]
ask --> block["No, or no answer:<br/>block"]
- The command is split into statements, at line breaks and at
;,&&,||and&. Judged as a whole, a long command hides its worst line. - Statements that are plainly read-only (
ls,cat,git status, …) are not sent anywhere. Neither is cleanup under/tmp, the most common false alarm. - Every other statement goes to the classifier together with its context: the
working directory and the values of variables set earlier in the command.
Deleting a directory just created with
mktempscores low. Deleting the session directory scores high. - If any statement scores above the threshold, the user is asked, with a countdown. Yes runs the command. No, or no answer before the countdown ends, blocks it. The model is told why and carries on with other work. A session never waits forever.
On the same 28 destructive commands it asked about 21. When the classifier cannot
answer (no key, timeout, error), confirm-destructive's patterns judge the
command instead, with the same countdown.
While erdc-guard is loaded it claims the shell-command check, and
confirm-destructive steps aside. Otherwise a command that erdc-guard let
through, or that the user approved, would still stop at the other extension's
untimed question. confirm-destructive keeps its session questions.
/guard switches it on or off and shows its settings. /guard test <command>
scores a command without running it.
Child agents are not guarded
Child agents started by task and fork run without extensions, so neither
gate checks their commands. The task
tool's write boundary is the check that applies to them.
Keeping secrets out of the model¶
secret-guard (pi-extensions) keeps the values of the container's secret
environment variables out of three places: what the model sees, what the session
file stores, and what is fed into the palace.
A variable counts as secret when its name says so (…TOKEN, …SECRET,
…API_KEY, …PASSWORD, …) and its value looks like a credential. Each exact
value is replaced with a placeholder such as <redacted:GITHUB_TOKEN>. It acts on
every tool result and on every request to the model. It also blocks a file write
that would replace a real secret with its placeholder, which is what an agent
would otherwise do after reading a redacted .env file.
Commands keep working: $GITHUB_TOKEN still expands in the shell, and only the
output that comes back is rewritten. Because it matches known values, not shapes,
it cannot misfire on text that only looks like a secret.
Why it exists: on 2026-10-12 a session printed its environment through a filter that missed one token. That value turned out to be in twelve session files going back two months, and each of those sessions had sent it to the model provider.
It does not see secrets that are not in its own environment, such as key files or another host's configuration. The next guardrail covers those for the palace.
Keeping credentials out of the shared palace¶
The palace is the fleet's shared memory (see Memory). It is copied to backups and to other machines, so a credential written into it cannot be removed by deleting it. The only remedy is to revoke the credential.
erdc-palace-check (fleet extension) asks ERDC's clef classifier, before every
write to the palace, whether the text contains a credential value. It checks the
fields that become stored text: memory contents, diary entries, event bodies and
attachments. Long text is split into pieces, because a lot of harmless text hides
one bad line.
Because the damage cannot be undone, it is stricter than erdc-guard. A flagged
write is blocked unless the user approves it in time, and the agent is told to
record the credential's name instead of its value. If ERDC cannot answer, the
write goes ahead and is marked as unchecked.
/palace-check switches it on or off. /palace-check test <text> scores a text
without writing it.
Keeping delegated work inside its boundary¶
An agent can hand part of a job to a child agent. There are two ways, and they differ in what the child sees:
forkgives the child the whole conversation. In a long session, the conversation outweighs the instruction. Forks have ignored prohibitions and quoted text that did not exist, and nothing checks what they changed.taskgives the child only a written specification: the goal, named files and verified facts. The directories it may watch and the ones it may write to are listed. Every watched directory is compared before and after, and a change outside the allowed set fails the task. Each run leaves an audit record.
fork-gate (pi-extensions) blocks a fork whose instruction contains a
prohibition (do not, never, only), a write boundary, or a request to change
files. In place of the fork it hands back the task call to use. It matches
wording, not intent, and says so: a read-only brief that trips it can be
rephrased.
Secrets in git¶
The image ships gitleaks, a scanner for credentials in files and commits. Repositories that sit close to secrets run it before every commit. docker-compose-repo does this with a tracked pre-commit hook. This site's CI scans every push.
Files that must hold secrets, such as each machine's environment file, are committed only encrypted with age. Only a host that holds a matching private key can decrypt them, and private keys are never committed.
When a credential leaks anyway¶
The baked credential-incident-response skill gives the agent a procedure. It
starts with the provider that issued the credential, because most exposed
credentials in a long-lived fleet are already dead. A live one may have more
access than anyone assumed. For anything already copied to other places, such as
the palace, it prefers revoking the credential over deleting the text.
Undoing an agent's edits¶
git-checkpoint (pi-extensions) saves the working tree of a git repository at the
start of every turn, without changing it. When you /fork from an earlier point in
the session, it offers to restore the files to that point.
Switches¶
| Guardrail | Turn off for a session | Off for one process |
|---|---|---|
erdc-guard |
/guard off |
ERDC_GUARD=off |
confirm-destructive |
/ext |
none |
secret-guard |
/ext |
PI_SECRET_GUARD=off |
erdc-palace-check |
/palace-check off |
ERDC_PALACE_CHECK=off |
fork-gate |
/ext |
PI_FORK_GATE=off (logs instead of blocking) |
Sources¶
- pi
docs/security.md("Run Pi safely") - pi-extensions
README.md: sectionsconfirm-destructive.ts,secret-guard.ts,fork-gate.ts,git-checkpoint.ts - docker-compose-repo
common/pi/erdc-guard.ts,erdc-palace-check.ts,erdc-classifiers.ts(file headers), andhooks/pre-commit - pi-devbox
Dockerfile.base(gitleaks) and the bakedcredential-incident-responseskill - pi-toolkit
bin/pi-task