Agent Autonomy Policy

What Claude agents may do on their own, what requires Sean, and — crucially — where each boundary is enforced. The lesson of the July 2025 Replit incident (an agent deleted a production database during an explicit code freeze) is that prompt-level instructions do not constrain agents. A rule that lives only in a prompt or a doc is a convention; a rule that lives in permissions, hooks, credentials, or CI is a boundary. This policy names both kinds honestly and tracks the gap. Split from the agent-ops sweep (#550) as issue #559.

Related: production/kamal.md (deploy flow), rails_secrets.md (why RAILS_MASTER_KEY is never in the tree), project_board.md (board workflow), manual_test_plan.md Appendix A (post-deploy smoke test).

Autonomy tiers

Tier 1 — autonomous

Agents do these without asking, as already practiced:

  • Create branches; write code, tests, migrations, docs; run the test suite, linters, and local rake tasks.
  • Open PRs and move the corresponding issue to In review on the board.
  • Comment on issues and PRs (gh issue comment, gh pr comment), including status reports and recommendation write-ups.
  • Read production logs/state read-only when explicitly pointed there by a runbook Sean invoked.
  • Dev-database content work (grammar notes, curricula) — changes reach production only through migrations/runner scripts that ride a reviewed PR and a human deploy.

Tier 2 — approval-gated (Sean acts, or explicitly instructs per instance)

  • Merges to main. Claude never merges; Sean reviews and merges every PR (the stacked-PR train convention from #280).
  • Production deploysbin/deploy, any kamal invocation, and pushing v* tags or creating GitHub releases (.github/workflows/deploy.yml deploys on v* tag push, so a tag push is a deploy).
  • Direct production VM access (ssh root@…, scp/rsync to the host).
  • Destructive database operationsdb:drop / db:reset anywhere (the dev DB holds a production copy that is expensive to reload), and any data-deleting runner script against production.
  • Spending — new paid services, plan changes, anything that adds a recurring cost. LLM API usage inside agreed workflows is Tier 1; changing which paid tier/model a recurring workload uses is Tier 2 (see #560).
  • External publishing and outbound communication — emails to users, posts under the Immersive name, TestFlight/Play releases, DNS/Cloudflare changes.

Tier 3 — forbidden without explicit per-instance instruction

  • Force pushes (--force, --force-with-lease, --mirror) and deleting remote branches or tags.
  • Changes to credential handling: editing config/credentials/*, .kamal/ secrets, bin/deploy’s secret resolution, or copying RAILS_MASTER_KEY anywhere in the working tree.
  • Deleting user data, anywhere.
  • Editing this policy’s enforcement (.claude/settings.json, .claude/hooks/) — an agent must never loosen its own guardrails.

Any violation of Tier 2/3 is an incident: write a one-page postmortem (what happened, what the agent claimed, what was actually true, which guardrail changed as a result).

Enforcement map

For each gated action: where the boundary actually lives today, versus where it is only convention.

Action Enforced by (today) Gap / recommendation
Merge to main Hook + denylist block gh pr merge in agent sessions No GitHub branch protection (unavailable on the current private-repo plan). Anything with push access can technically push to main. Recommend: enable branch protection (require PR + review) when the repo goes public or the plan is upgraded.
bin/deploy / kamal Denylist + hook block the commands. bin/deploy also needs .env secrets and the SSH key. The blocks live in .claude/settings.json, which only governs Claude Code sessions in this repo. An agent shell still inherits Sean’s SSH agent and .env. Recommend (later): a deploy-only machine identity, so agent-reachable credentials cannot reach the VM at all.
v* tag push / gh release Hook blocks git push --tags, v* refs, and gh release create/delete — because deploy.yml deploys on tag push. Tag push from any other terminal is unguarded; acceptable, since only Sean uses other terminals.
Production VM access Hook blocks ssh/scp/rsync targeting root@. Origin firewall limits 80/443 to Cloudflare (production/kamal.md). Same caveat: the SSH key itself is agent-reachable. Covered by the machine-identity recommendation above.
Production RAILS_MASTER_KEY Real infrastructure boundary: never in the tree or .env (rails_secrets.md); production credentials are undecryptable in an agent session. None — keep it this way. This is the model the other boundaries should converge on.
Destructive DB ops Hook blocks db:drop / db:reset / db:truncate_all. Production Postgres is not network-reachable (Docker-internal only; access requires SSH, which is blocked). Nightly VM backups exist (lib/tasks/backup.rake); recommend a periodic verified restore drill so “restorable” is a tested fact, not an assumption.
Spending No billing-console credentials exist in any agent-reachable location. LLM API keys are agent-reachable spend. Recommend: monthly spend line + budget alarm (#560).
Outbound email / publishing SMTP credentials live only in production credentials (undecryptable locally). Dev fakes email; FakeAI fakes AI. Real boundary for email. Social/blog publishing has no agent-reachable credentials today; keep it that way.
Force push / branch deletion Denylist + hook block git push --force/-f/--mirror/--delete. Prefix-matching permission rules can be evaded by argument reordering; the hook parses arguments, which is stronger but still not a security boundary against a determined adversary. Acceptable for the actual threat (agent error, prompt injection making an agent try something).

Honesty note: the denylist and hook are defense-in-depth against agent mistakes and prompt injection, not a sandbox. The only boundaries that hold against an arbitrarily wrong agent are credential ones: secrets that do not exist in the agent’s environment. Every recommendation above pushes in that direction.

What is enforced in-repo

.claude/settings.json (checked in, applies to every Claude Code session in this repo) carries two layers:

  1. permissions.deny rules — fast, first-line prefix matches: bin/deploy, kamal, bin/kamal, git push --force/-f/ --force-with-lease/--tags, gh pr merge, gh release create/delete, bin/rails db:drop/db:reset.
  2. A PreToolUse hook (.claude/hooks/bash_guard.rb) — parses every Bash command, splits compound commands (cd x && bin/deploy is still caught), strips bundle exec/env-var prefixes, and blocks the same actions regardless of argument order. It fails open on malformed input (the denylist still applies) and prints the tier and reason so the agent can relay why to the user.

If Sean genuinely wants an agent to run a gated command, he removes the rule for that session deliberately. The friction is the point.

Lethal trifecta audit

Simon Willison’s frame: an agent that combines (A) exposure to untrusted input, (B) access to secrets/private data, and (C) the ability to write externally is exploitable via prompt injection regardless of prompt guardrails. No single agent context should hold all three.

None of the four agent definitions in .claude/agents/ sets a tools: frontmatter key, so all currently inherit the full toolset (Bash, gh, web access) of the session that spawns them. The audit below is by practiced role; the recommendation is to make it structural.

Agent A: untrusted input B: secrets C: external writes Trifecta?
Default Claude Code session Yes — web search/fetch, issue text Yes — .env (GHCR token), dev credentials, SSH agent Yes — git push, gh Yes. Mitigations: this policy’s hook + denylist gate the dangerous writes; Sean reviews every merge; RAILS_MASTER_KEY unreachable. Residual risk accepted for the working session.
cefr-curriculum-expert Yes — researches exam syllabi on the web Inherited (full toolset) DB writes (dev) + inherited gh Yes, structurally. Should be: research + dev-DB only. Recommend tools: without web fetch when writing, or without gh entirely.
cefr-grammar-author Low — works from prompts + dev DB Inherited DB writes (dev); content reaches learners only via reviewed PR + human deploy + the #558 quality gate Borderline. The learner-facing write path is human-gated, which is the mitigation that matters. Recommend tools: denying web fetch so untrusted web text can never enter grammar notes unvetted.
hotwire-native-expert Yes — official docs on the web Inherited; TestFlight/Play shipping needs Sean’s Xcode signing (real boundary) Inherited gh; store releases human-gated Partial. Store publishing is credential-gated (good). Recommend tools: limiting to read + local build.
ux-designer Yes — rendered pages at localhost:3000 (dev DB holds production user content) Inherited Screenshots + review comments; inherited gh Partial. Needs Playwright + Read only. Recommend the narrowest tools: list of the four.
@claude GitHub Action (claude.yml) Yes — any collaborator’s issue/PR text OAuth token only No — workflow permissions are contents: read, issues: read, pull-requests: read No. Read-only token is a real boundary; keep it read-only.

Structural rule going forward: research agents get no write credentials; content agents that write to the DB do not fetch arbitrary web content in the same session; outbound actions stay human-triggered. When adding a new agent definition, give it an explicit tools: list and add a row to this table in the same PR.


This site uses Just the Docs, a documentation theme for Jekyll.