Guard
The authority envelope and how decisions are made.
Press 6. Optimize decides what Claude can see; Guard decides what it can
do. Guard runs as a Claude Code PreToolUse hook, which means it returns a decision
before the tool executes rather than reporting on it afterwards.

The authority envelope
The envelope is the task's authority. It is stored outside the conversation, at
~/.iris/projects/<id>/sessions/authority.json, for one reason: the model must not be
able to widen its own permissions by writing convincing text. It may propose an envelope; a
proposal that grants more than the current one is rejected.
{
"scope": {
"filesystem": ["$PROJECT/**"],
"environments": ["staging"],
"network": [],
"destructive": "ask",
"production": "deny",
"credentials": ["staging"],
"externalWrites": "ask"
}
}
Those are the defaults, written by init if no envelope exists yet. The path can be
overridden with IRIS_ENVELOPE_PATH; the hook, the UI and the server all read the same
file, so there is exactly one source of truth.
Step 1 — normalize the effect, not the string
Matching on command text is how you end up denying
echo "the word production appears in this string". Iris instead compiles the tool call
into a structured effect:
{ effect: "delete", resourceType: "cloud", service: "aws",
environment: "production", external: true, destructive: true,
reversible: false, credential: "production", unknown: false }
Before classifying, it strips the parts of a command that are data rather than targets — heredoc bodies, comments, and prose inside quotes — so a deployment keyword in a log line is not read as a deployment target.
Dedicated recognizers cover filesystem tools, Railway, AWS, git, npm/yarn/pnpm publish and
install, databases (psql, mysql, mongosh,
redis-cli), curl/wget, and rm. Anything else falls
to a read-only allowlist of 58 inspection commands (ls, grep,
cat, head, find, jq, awk,
stat …).
A denylist can only catch the shapes it has been taught, and silently allows the rest. So
npx some-unknown-cli is not "probably fine" — it is unknown: true, and
unknown means ASK.
The allowlist revokes itself when a reader is turned into a writer: sed -i,
find -delete, any > redirect, or any $(…) command
substitution drops the command out of read-only.
Step 2 — evaluate against the envelope
evaluate() walks a fixed ladder and returns the first match. Order matters: a hard
deny is checked before anything can be explicitly allowed.
| # | Rung | Fires on |
|---|---|---|
| 1 | Hard deny | production env · production credentials · denied destructive · denied external writes |
| 2 | Scope violation | path outside $PROJECT · env not in scope · host not in allowlist |
| 3 | Authority expansion | staging-scoped session reaching for production · credential discovery beyond the envelope |
| 4 | High consequence | unknown effect · destructive · external write · credential use → ASK |
| 5 | Explicit allow | in-project non-destructive file ops · known-safe local commands |
| 6 | Default | ASK |
Step 3 — what that produces
Real output against the default envelope:
| Tool call | Decision | Rule |
|---|---|---|
Read src/guard/policy.mjs | ALLOW | explicit-allow.in-scope |
Edit src/guard/effects.mjs | ALLOW | explicit-allow.in-scope |
npm test | ALLOW | explicit-allow.in-scope |
git status --short | ALLOW | explicit-allow.in-scope |
rm -rf ./build | ASK | high-consequence.destructive |
npx unknown-cli --wipe | ASK | high-consequence.unknown |
git push origin main | ASK | high-consequence.external-writes |
Write /Users/dev/.ssh/config | DENY | scope.filesystem |
npm publish --access public | DENY | hard-deny.production |
aws s3 rm s3://acme-prod-assets --recursive | DENY | hard-deny.production |
psql postgres://prod-db.internal/app -c "DROP TABLE users" | DENY | hard-deny.production |
Note what is not denied. git push origin main asks rather than blocks — a
branch name is not an environment, and treating it as one made every ordinary push a "production
deploy". npm test allows — classifying it as an install made it prompt on every run. A
guardrail that cries wolf gets switched off.
Step 4 — trajectory signals
Single calls are not the whole risk. A session that reads, fails, retries, then reaches for a production credential is a pattern, and Guard tracks it across the session.
| Signal | Fires when |
|---|---|
AUTHORITY_EXPANSION | environment escalates (staging → production) |
CONSEQUENCE_ESCALATION | read/write turns into destructive |
CREDENTIAL_SCOPE_CHANGE | credential scope widens, or a secret is discovered after a failure |
RECOVERY_ESCALATION | recovery depth ≥ 2 while effects keep escalating |
PRODUCTION_DESTRUCTIVE | destructive effect aimed at production |
What happens when policy cannot load
Missing envelope, unreadable envelope, malformed JSON, or an unclassifiable command all resolve to ASK or stricter. There is no path through the hook that produces a silent allow, and every decision — including the failure — is written to the ledger.
Known limitations
It is another boundary around the agent, strongest on filesystem scope and production access. It should not be the only protection standing between an agent and irreversible infrastructure.
- Arbitrary shell is not perfectly classifiable. Recognizers catch known dangerous shapes; a deliberately unusual command may still land in ASK rather than DENY.
- The prose-stripping heuristic is a heuristic. It narrows a known false-positive class. It is not a shell parser.
- Guard sees tool calls, not consequences. A command it allows can still do something you did not intend, if that something is inside the envelope you accepted.