Intro to agents¶
Last updated: 2026-04-13 · Reading time: ~20 min · Difficulty: moderate
TL;DR
- An agent in Clawford is a directory of markdown files plus a
manifest.json. There is nothing magical under the hood — if you can read eight markdown files, you can read an agent. - The fleet draws a hard line between LLM reasoning (compose, rank, judge) and deterministic Python (scrape, auth, execute). Know which side of the line a given piece of work belongs on.
- There are two kinds of crons: OpenClaw crons run inside the container and call the LLM; host crons run on the VPS itself and don't. The canonical host cron is the fleet-health probe, which is how Mr Fixit learns the rest of the fleet is still alive.
- Every script that a cron calls must follow the script contract: print one JSON line with a
statusfield, alwaysexit 0. Exit codes are for operators, not for LLMs. - I run a permissive exec-approvals policy. I did not start there. The honest motivation is in the section below, and it's the single design decision in this chapter most worth understanding before you disagree with it.
An agent is a directory of files¶
Every agent in Clawford is a directory on the VPS that holds:
- Eight markdown workspace files that OpenClaw auto-loads at the start of every LLM session.
- A
manifest.jsonthat declares everything about the agent — which bot it owns, which crons it runs, which scripts those crons are allowed to call, which on-disk state files it owns, and what its exec-approvals look like. - A
scripts/directory of deterministic Python (and occasionally shell) that the LLM invokes to touch the real world — browsers, HTTP, filesystem, Gmail, payment portals. - A
tests/directory of red/green TDD tests for everything inscripts/.
If you're ever confused about what an agent is, open agents/fix-it/ in a file browser. The answer is "that directory." There is no hidden sidecar and no remote state you can't see.
The eight workspace files¶
The workspace files are the agent's stable cross-session state. OpenClaw loads all eight at the start of every cron tick or message, so an agent that doesn't have them doesn't know who it is.
| File | What it holds |
|---|---|
SOUL.md |
Values, boundaries, operating model. The "why" of the agent. Made immutable on the VPS after deploy (chattr +i) because prompt injection will try to rewrite it if you don't. |
IDENTITY.md |
Name, emoji, tone, catchphrase. Who the agent is. Also immutable. |
TOOLS.md |
Which scripts are available, what they do, when to use which. The LLM reads this to answer "how should I accomplish task X?" |
AGENTS.md |
Hard rules, role definition, config architecture, the fleet roster — so one agent knows who its coworkers are. |
USER.md |
The human's name, timezone, preferences, communication style. Keeps the agent calibrated to you instead of to a generic user. |
HEARTBEAT.md |
The lightweight 30-minute checklist — things to verify on every heartbeat tick without burning the full LLM budget. |
MEMORY.md |
Persistent lessons learned from experience — "NEVER re-enable X, ALWAYS use Y." This is where scar tissue lives. |
BOOTSTRAP.md |
First-run only. Instructions for the agent's first session. The agent is supposed to delete this file once it's no longer needed. A lingering BOOTSTRAP.md puts the agent into a split-brain state — the correct SOUL.md and IDENTITY.md sit right there on disk, but the LLM reads the bootstrap onboarding script instead and starts responding to messages like a blank-workspace rookie. The Pitfalls section at the bottom of this chapter has the full war story. |
SOUL.md and IDENTITY.md get made immutable at the filesystem level (chattr +i) after they're deployed. Soft prompt-level rules are not strong enough — the LLM will happily rewrite its own soul if you ask it to directly. The OS-level lock is the backstop. See Ch 08 for the rest of the hardening story.
manifest.json is the whole agent, declared¶
Each agent has a manifest.json sitting next to the workspace files. It's the single source of truth for how the agent gets deployed. Trimmed shape:
{
"agent_id": "shopping",
"display_name": "Hilda Hippo",
"workspace": "~/.openclaw/shopping-workspace",
"telegram": {
"account": "shopping",
"bot_token_env": "SHOPPING_BOT_TOKEN"
},
"config_files": [
{ "src": "SOUL.md", "immutable": true },
{ "src": "IDENTITY.md", "immutable": true },
{ "src": "TOOLS.md" },
{ "src": "AGENTS.md" },
{ "src": "USER.md" },
{ "src": "HEARTBEAT.md" },
{ "src": "MEMORY.md" },
{ "src": "CRONS.md" }
],
"scripts": [
"scripts/grocery.py",
"scripts/amazon-orders.py",
"scripts/costco-token-daemon.py"
],
"state_files": [
{ "path": "grocery-list.json", "seed_if_absent": { "items": [] } }
],
"approvals": {
"allowlist": ["/usr/bin/*", "/bin/*", "/usr/local/bin/*"]
},
"crons": [
{
"name": "delivery-digest",
"cron": "0 14 * * *",
"message": "…exec-discipline preamble + the day's work…",
"announce": true
}
]
}
When you run agents/shared/deploy.py <agent-id>, it reads this file and does the full install: copies the workspace files into ~/.openclaw/<agent>-workspace/, seeds state files, registers crons with OpenClaw, binds the Telegram bot, writes exec-approvals, and chattr +is the immutable files. It also produces a pre-deploy backup tarball so a bad deploy is tar -xzf away from a recovery. deploy.py has ten safeguards layered on top of that — see DEPLOY.md and Ch 05 for the full tour.
⚠️ Warning.
manifest.jsonis.gitignored; the checked-in version ismanifest.json.example. On first-time setup you copy the example and fill in your real values per Ch 02's.examplepattern.
LLM vs deterministic — where the line sits¶
The most important design rule in Clawford is that the LLM does not touch the real world directly. The LLM reasons; deterministic code acts.
- LLM work (inside OpenClaw): composing digests, ranking items, summarizing threads, making judgment calls, deciding whether to notify you, drafting Telegram messages, picking which script to call next.
- Deterministic work (Python subprocesses): scraping websites with Playwright, handling auth and multi-factor re-auth, calling the Gmail API, writing to files, talking to external APIs, parsing HTML, validating JSON, writing state.
The seam between the two is plain JSON. A cron message tells the LLM: "run python3 <absolute-path-to-script>, read the final JSON line from stdout, decide what to do next." The script is responsible for doing the work and reporting structured results. The LLM is responsible for understanding what those results mean in the context of the user.
This line is what keeps the system debuggable. When something goes wrong, the first question is always "is it the script or the LLM?" and the answer is almost always visible in whatever the script printed to stdout. If the script emitted {"status": "ok", "orders": [...]} and the LLM still sent you a confused Telegram message, the bug is above the seam. If the script emitted {"status": "error", "error": "auth_failed"} and the LLM ignored it, that's a prompt-engineering bug. Either way, the JSON is the contract — and because it's the contract, you (or Claude) can stare at it by hand when something breaks.
Two kinds of crons¶
Not every cron in the fleet runs the LLM. There are two distinct cron types and it matters which one a given task belongs to.
OpenClaw crons live inside the OpenClaw container. They're declared in an agent's manifest.json and they fire an LLM session on a schedule. Each one gets a prompt (the cron's message), the full 8-file workspace, and a hard 600-second budget. They're expensive and flexible — use them when the task needs judgment.
Host crons live in the VPS's own crontab (or a systemd timer). They run plain Python or shell and never call the LLM. They're cheap and deterministic — use them when the task is "run this script on a schedule and write the result to a known file."
The canonical host cron is fleet-health. A host-level script — ops/scripts/fleet-health-host.sh — probes each agent's workspace every few minutes, checks that the cron daemon is alive and that the agent's most recent activity is recent enough, and writes a snapshot to fleet-health.json. Mr Fixit reads fleet-health.json on his own tick and tells you if anything is off.
This used to be an LLM-driven cron — a per-agent heartbeat-check and an LLM morning-status composer — and it got retired because it was both expensive (LLM budget) and unreliable (recursive docker exec from inside the container hit race conditions I never fully understood). Moving the probe to the host was the fix. The general design rule from that refactor: anything that has to observe the container from outside should live on the host. If you find yourself writing a cron that shells out to docker exec <self> to inspect its own container, that's a signal to move the cron.
🔦 Tip.
fleet-health.jsonis the canonical "is the fleet alive" signal. If you see code parsingagents/*.status.mdfiles for health state, it's reading a legacy surface — those per-agent status files still exist and individual agents still write into them, but they are no longer the source of truth for fleet health. Ch 05 has the full brain-state map and the reasons.
The script contract¶
Every script that an OpenClaw cron invokes must follow one shape:
- The final non-blank stdout line is a single JSON object with at least
{"status": "ok" | "error" | "degraded"}. - Always
exit 0from main, even on failure. Catch the exception, print an error-shaped JSON object, then exit 0. - Stderr is free. Use it for debug logs — OpenClaw doesn't parse it, so it won't interfere with the stdout contract.
- Run as a bare
python3 <absolute-path>. No shell wrappers, no pipes, no redirects, nosh -c, no; echo $?.
The full spec and a skeleton template live in agents/shared/SCRIPT_CONTRACT.md. The test harness in agents/shared/tests/test_script_contract.py enforces the contract — statically by walking every manifest's cron messages and rejecting forbidden shell operators, and at runtime by running every script in isolation and asserting the output shape.
The reason the contract is so pedantic is scar tissue. OpenClaw 2026.4.11 added a hardcoded exec preflight that rejects any python3 <...> command matching shell operators — ;, &&, output redirects, sh -lc, exit-code capture, all of it. When an LLM running a cron session reflexively wraps a command as python3 script.py; printf "EXIT:%s" $? to "also check the exit code," that command gets hard-rejected, and before the contract existed, exactly that wrapping cascaded the entire six-agent fleet into exec blocked by approval policy messages in a single morning. The durable fix had two sides: scripts started self-reporting status via JSON (removing the LLM's reason to wrap), and cron messages started opening with an explicit "run this bare, do not append anything, do not capture exit codes" preamble (removing the temptation). Both sides are load-bearing.
🧨 Pitfall. Any LLM session that wants to "also capture the exit code" of a python command. Why: OpenClaw's preflight rejects it, and every workaround you think of will break something else. How to avoid: teach the agent — in
TOOLS.md, in the cronmessagepreamble, and inMEMORY.md— that exit codes are for operators, not for LLMs. The status lives in the final JSON line. If Claude Code is drafting a cron message for you, watch for reflexive; echo $?and delete it every time.
Exec-approvals, and why mine are permissive¶
Each agent has a per-agent exec-approvals policy stored on the VPS at ~/.openclaw/exec-approvals.json and baselined in git at ops/exec-approvals-baseline.json. Simplified, the baseline says "every agent runs in the maximally permissive mode — no security gating, no approval prompts":
{
"defaults": { "security": "full", "ask": "off" },
"agents": {
"fix-it": { "security": "full", "policy": "full", "ask": "off" },
"shopping": { "security": "full", "policy": "full", "ask": "off" }
}
}
I understand how that reads on first look. Hear me out.
I did not start permissive. I started with strict approval gating and hit approval prompts constantly — commands that should have worked got prompted, allowlist patterns had edge cases, the LLM would construct shell pipelines that no regex could preemptively bless. The irreparable straw was realizing that shells themselves break allowlists. A command like sh -c "cmd1 && cmd2" evades pattern-matching because the matcher sees sh, not the inner command. You can try to disable sh. The LLM will find bash. Disable both, it will find python3 -c "os.system(...)". You cannot pattern-match your way out of a Turing-complete shell language, and the harder you try, the more you train the LLM to route around you.
So I gave up on pattern-matching shells as the primary defense and moved the defense to three other layers:
- OS-level immutability on identity.
SOUL.mdandIDENTITY.mdarechattr +i. The agent cannot rewrite its own values even if a prompt-injection attack tells it to, because the filesystem refuses. - Baseline drift detection. The exec-approvals file is committed to git and checked by
deploy.pySafeguard 8 on every deploy. If the live file on the VPS has drifted from the baseline — say an OpenClaw upgrade silently tightened something, or someone hand-edited the file on the VPS — the deploy refuses until you reconcile the drift. That catches "silent post-upgrade allowlist change" before it breaks your crons in the morning. - The script contract. Scripts always
exit 0and report status via JSON. Combined with cron preambles that forbid shell operators, there's no path for an LLM to catch a failed command via a nonzero exit code and try to "recover" it by escalating.
Permissive exec-approvals + immutable identity + baseline drift detection + script contract is the pragmatic landing spot for my threat model, which is that my own overnight Claude sessions and prompt-injection-through-scraped-content are more likely threats than the LLM spontaneously deciding to exfiltrate my files. Your threat model may be different. If you want strict approvals instead, you can set them — I'd just recommend reading Ch 08 first so you understand which threats that actually mitigates and which ones it doesn't.
A few things that will bite you before Ch 07¶
Three small lessons I paid for, worth knowing before you try to deploy a real agent:
The 600-second LLM cron budget is a hard ceiling¶
OpenClaw crons are killed at ten minutes. For anything that might hit that budget — especially structured-output tasks that want to reason over a lot of content — do not write it as an in-container LLM cron. Write it as a Python composer that calls openclaw infer directly with a structured-output schema, and run the composer from a host cron. The composer pays only for what it uses and isn't bounded by the cron session budget. Lowly Worm's morning news digest ended up this way after the LLM-cron version kept getting killed mid-compose; Ch 07-2a has the story.
Google OAuth — auth on your laptop, not on the VPS¶
Google's OAuth flow requires a browser, and the VPS doesn't have one. The pattern I landed on: run the auth flow locally on your laptop, grab the resulting token.json, and scp it to the VPS. You'll also need to add your Google account as a test user on your Cloud project before the first auth — Google's consent screen rejects non-listed users during testing mode, and the error message is unhelpfully generic. Use a Desktop OAuth client type, not Web — Web clients require an HTTPS redirect URI the VPS doesn't have. Every Google-touching agent (Sergeant Murphy, Mistress Mouse, Huckle Cat) shares this flow.
Never claude -p from inside an agent cron¶
Agent crons should never shell out to the Claude CLI. Use OpenClaw's native LLM (via openclaw infer or the cron's own session) or the LLM provider you chose in Ch 02. The clearest reason is terms of service: Anthropic's Claude Code legal-and-compliance doc explicitly says "Anthropic does not permit third-party developers to offer Claude.ai login or to route requests through Free, Pro, or Max plan credentials on behalf of their users" and directs developers building anything that interacts with Claude's capabilities — including the Agent SDK — to use API-key auth through the Claude Console instead. Wiring an agent up to claude -p against a Pro or Max plan is picking a fight you will lose. The secondary reasons are billing (you don't want per-cron Claude calls charged to a subscription that was scoped for interactive use) and debuggability (an agent calling a CLI calling another LLM stacks so many sessions that an error in the middle becomes unreadable). The rule is simple: agents talk to OpenClaw and to their scripts; they don't talk to other LLM CLIs.
Pitfalls you'll hit¶
🧨 Pitfall. Writing an LLM cron that tries to do 600 seconds of structured-output work in a single session. Why: OpenClaw hard-kills crons at ten minutes, and you'll discover this the first time your digest composes 80% of a report and then vanishes. How to avoid: if the task has a defined output schema, run it as a host cron that calls
openclaw inferfrom a Python composer. LLM crons are for judgment under time pressure, not for long-form reasoning.🧨 Pitfall. Writing an agent that reads
agents/<name>.status.mdfiles to check on its coworkers. Why: those files are legacy — they still get written but they are not the fleet-state source of truth anymore. A parser that globs*.status.mdis reading stale data. How to avoid: readfleet-health.jsoninstead, written by the host-cron probe. Ch 05 has the full state map.🧨 Pitfall. A stale
BOOTSTRAP.mdputs the agent into a split-brain state. Why:openclaw agents add(or any partial run of it) writesBOOTSTRAP.mdinto the workspace as a first-run onboarding script. At session load time that file takes precedence overSOUL.mdandIDENTITY.mdbecause it's the designated "fresh boot" prompt. Result: the deployed identity files on disk are correct, but the running LLM behaves like an empty agent and introduces itself to you with "I just came online. What should I call you?" — the blank-workspace onboarding dialog, even though the real identity is one file over. I chased this symptom for an embarrassing number of hours the first time it hit, because everything on disk looked fine. How to avoid:deploy.pydetectsBOOTSTRAP.mdand removes it automatically whenever the manifest includesIDENTITY.md. If you're deploying manually, deleteBOOTSTRAP.mdyourself after the first successful onboarding. If an agent ever starts greeting you like a stranger and the workspace files on disk look correct,ls ~/.openclaw/<agent>-workspace/BOOTSTRAP.mdis the first thing to check.
See also¶
- AGENTS-PATTERN.md — the repo's canonical list of workspace files, deployment invariants, and human-facing rules. Keep this open while you read Ch 07.
- agents/shared/SCRIPT_CONTRACT.md — the full script contract, the skeleton template, and the migration guide for pre-contract scripts.
- ops/exec-approvals-baseline.json — the committed baseline the deploy tool checks drift against.
- agents/shopping/manifest.json.example — the canonical manifest example with real cron message patterns and the exec-discipline preamble.
- DEPLOY.md — what
deploy.pyactually does, step by step, with the ten safeguards enumerated. - Ch 05 — Infra setup — where
fleet-health.json, the shared brain layout, anddeploy.pyitself are documented. - Ch 08 — Security and hardening — the defense-in-depth story that sits on top of the permissive exec-approvals choice.