The agent charter: steer an autonomous agent with one file
By Albert Nahas, 9 min read, updated
To run an autonomous coding agent safely, give it one written charter that it reads first on every run. The charter holds its mission, a kill switch it checks before anything else, the data it may read, KPIs with alert thresholds, a merge policy and the decisions only a human may make. The guardrails that don't depend on the model cooperating sit outside that file: turn and budget caps on the CLI, a tool allowlist, and a runner script that won't start the agent when the switch says paused.
I run scheduled Claude Code agents on my live product, OpenWhen, a site more than 70,000 people have used. Each agent is steered by a charter like this. This is the structure I've settled on after many months of runs, including the ones that went wrong.
Why a charter beats a better prompt
A prompt saved inside a scheduled task is invisible. To change the agent's behavior you edit the task, and nothing records what changed or why.
A charter in the repo fixes that. The scheduled task's prompt stays short: read docs/OPS_LOOP.md and follow it. The charter's second line says it's the single source of truth and overrides the base prompt. I steer the agent by editing the file, never the routine.
That gives me three things a prompt can't:
- Version history. Every change to the agent's behavior is a commit with a message.
- Reviewable steering. The agent can propose edits to its own charter as a PR, and the change goes through the same review as code.
- One place to look. When a run does something odd, I read one file to see what it was told.
The skeleton
Here's the shape, trimmed to what every charter of mine has:
# Daily Ops Loop: Charter
status: active
Single source of truth for the daily ops agent; overrides its base prompt.
## Mission
Operate the product like a growth team: watch the numbers daily, find what
blocks growth, ship small safe fixes, escalate what needs a human.
## Kill switch
1. Pause: set `status: paused` above and push to main.
2. Terminate: disable the scheduled task.
## Data sources
Read-only access, how to query it, known quirks of each source.
If a source fails, write a report saying so and stop. Never change code blind.
## KPIs
| KPI | Baseline | Alert if |
## Merge policy
A / B / C / X grades. X waits for a human.
## Each run
Support first, then PR sweep, then KPIs, then at most 2 new PRs.
## Report
Format, hard length cap, escalation ledger.
## Operating principles
Data first. One change at a time per surface.
The kill switch, honestly
A status: line that the agent reads is a request. Most of the time the model honors it. But "most of the time" isn't a kill switch. If the agent reads the charter after something else has already run, or misreads it, the line does nothing.
So I use two levels:
- Soft pause in the charter.
status: activeorstatus: pausedsits on line 3, and the charter says to check it first on every run. Pausing a cloud agent is one edited line and a push. - Hard stop outside the model. For my local agents, the runner script checks the line before Claude even starts:
#!/usr/bin/env bash
set -euo pipefail
CHARTER="docs/AGENT_LOOP.md"
if ! grep -q '^status: active' "$CHARTER"; then
echo "$(date -u +%FT%TZ) paused by charter, not starting" >> logs/agent.log
exit 0
fi
claude -p "Read $CHARTER and run it." \
--max-turns 120 \
--max-budget-usd 15 \
--permission-mode auto \
--permission-prompts none
The cloud agent's hard stop is disabling the scheduled task itself (Claude Code calls them scheduled tasks now, formerly routines). For a launchd agent it's launchctl unload. Write the exact command into the charter's kill switch section, so that at 2am you aren't searching for it.
Limits that don't need the model's cooperation
The charter steers. These flags enforce:
--max-turnscaps how many agentic turns a run can take.--max-budget-usdstops the run when its estimated spend reaches the cap.--allowedToolsandpermissions.denyrules decide which tools run without asking. Denygit push --forceand anything that touches production secrets.--permission-prompts none(v2.1.259 and later) denies anything that would prompt and tells Claude not to retry it. Nobody is awake to answer the prompt anyway.--bareskips auto-discovery of hooks, skills, plugins, MCP servers and CLAUDE.md, so a scripted run behaves the same on every machine. Pass the context you need explicitly.
For loops that run task after task, I added a circuit breaker to my open-source autonomous-claude-queue: an iteration cap, plus stall detection that stops the queue when runs stop making progress. A loop that repeats the same failed fix 30 times is the most expensive kind of failure, and it's simple to detect from outside.
Data sources: read-only, and say when they fail
The data section tells the agent exactly how to read each source and what its quirks are. Quirks matter more than the queries. Mine includes notes like "this API caps a select at 1,000 rows, paginate" and "count a landing only if the same visitor fires another event within 10 minutes", because each one was a wrong conclusion in an earlier report.
The rule I'd copy first: if a source fails, write a report saying so and stop. Never change code blind. An agent that can't see the numbers will still find something to "fix".
The second lesson came from an incident: a hung probe is not an outage on its own. Over two days in August 2026, two runs froze on database timeouts and reported users as blocked. In the same window, real users kept creating content as usual. The database was slow, not down. The charter now says to check whether real activity is still landing before calling an outage, and to report degradation instead.
KPIs with alert thresholds
The KPI table is what turns "look at the numbers" into decisions. Three columns (the numbers here are illustrative):
| KPI | Baseline | Alert if |
|---|---|---|
| Signups per day | 300 (reference week) | below 240 for 2+ days |
| Checkout success rate | 10% | below 7% for 2+ days |
| Client errors on key pages | 0 | any new signature |
| Payments recorded on a day with checkouts | at least 1 | 0 |
The last row is the one I'd add to any product that takes money. Cumulative KPIs stay green through a day when checkouts happen but no payment is recorded. A dedicated row catches a broken webhook the same day.
The most expensive lesson in my charter is about baselines: never rebase a reference down, or a stall reads green. At one point an agent of mine wrote 27 clean reports in a row while a key KPI sat well below its old level. The reference had been rebased down to the lower run, so the alert never fired. The charter now names that KPI's reference period and forbids lowering it.
Two more rules sit under the table. Any KPI down more than 20% week over week is priority zero and leads the report. And before raising that alert, check that the comparison week wasn't a holiday or a launch spike.
Escalations: what only a human decides
The charter lists what the agent may never do on its own. In my merge policy those are grade X changes: money movement (checkout, webhooks, purchases, prices, refunds), migrations that rewrite or drop data, user identity, auth and secrets, new dependencies, paid-API spend and deleting user data. An X PR stays open with a founder label, the reviewer's findings as a comment, and one line in the report.
Everything else the agent decides. Copy, UI, admin pages, product limits and edits to the charters themselves are explicitly not founder calls. Writing the "not" list matters as much as the X list: an agent that escalates everything buries the few escalations that need you.
Escalations go in a ledger that carries forward between reports:
1. Emails suppressed by quota on 3 of 7 days - first raised 2026-09-03 - upgrade the email plan - monthly fee
2. Payment row never reconciled - first raised 2026-10-12 - RESOLVED - nightly job re-run
Each item stays until it's marked RESOLVED or DECIDED. Decided questions go into a separate decisions file, and the charter tells the agent not to relitigate anything listed there. Without that file, a scheduled agent with no memory will re-propose the same rejected idea every few runs.
The report is the agent's memory
Scheduled runs start with no memory of yesterday, so the report is the memory. Every run reads the last three reports first and deletes older ones in the same commit (git keeps them).
I cap the report at 80 lines with no line over about 300 characters, and the charter says to state each number once. Long reports bury the one finding that matters, and the next run has to read them too. The fixed order is: summary, support tickets, KPI table, ranked findings (max 5), actions taken, escalation ledger, handoff for tomorrow.
Scope per run
The charter also caps output: at most 2 new PRs per run, each small and focused, no refactors for taste. Every action has to trace back to a number in that day's data. One change at a time per surface, so tomorrow's run can tell what moved the number.
These limits feel conservative. They're why a long run of merged PRs didn't turn into a long list of things to untangle.
What I'd do next
Write the kill switch and the "never change code blind" rule before anything else, then put the agent on a schedule with this setup. Once it opens PRs, give it the A/B/C/X merge policy so you only see the changes that need you.
Questions people ask
How do I add a kill switch to an AI agent?
Use two levels. Put a status: active or paused line at the top of the agent's charter and have the runner script check it before starting Claude, then keep a hard stop outside the model, such as disabling the scheduled task or unloading the launchd job.
What guardrails should an autonomous coding agent have?
A turn cap and a budget cap on every run, a tool allowlist with denies for force pushes and secrets, a rule to stop when its data sources fail, and a list of changes it must never merge on its own, such as payments, migrations, auth and new dependencies.
How do I run an autonomous coding agent without it going off the rails?
Keep each run small: read the charter, act on today's numbers, open at most a couple of focused PRs, and have an independent reviewer grade each one. Have the agent read its last few reports first so it knows what it did yesterday.
Should the agent's instructions live in the prompt or in a file?
In a file in the repo. The scheduled prompt just says to read and follow it, so every behavior change is a commit you can review and revert.
How does Claude Code stop a run that is costing too much?
Pass --max-budget-usd to claude -p and the run stops when its estimated spend reaches the cap. Combine it with --max-turns for a second, independent limit.