Skip to content

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"]
  1. The command is split into statements, at line breaks and at ;, &&, || and &. Judged as a whole, a long command hides its worst line.
  2. Statements that are plainly read-only (ls, cat, git status, …) are not sent anywhere. Neither is cleanup under /tmp, the most common false alarm.
  3. 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 mktemp scores low. Deleting the session directory scores high.
  4. 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:

  • fork gives 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.
  • task gives 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: sections confirm-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), and hooks/pre-commit
  • pi-devbox Dockerfile.base (gitleaks) and the baked credential-incident-response skill
  • pi-toolkit bin/pi-task