The harness
The harness is everything Peen tells the model besides your message: AGENTS.md instructions, rules, skills, named agents, event handlers, and hooks. You write it as plain files beside your code. Peen reads those files fresh at the start of every turn and whenever an agent calls changeDirectory. An edit takes effect on the next message or directory hop without a restart. The two exceptions are SYSTEM.md and APPEND_SYSTEM.md, which Peen reads once when it starts.
| Piece | File | What it does | Details |
|---|---|---|---|
| Instructions | AGENTS.md | The project’s instructions file in the agents.md format, always in the system prompt | AGENTS.md and rules |
| Rules | .agents/rules/**/*.md, .claude/rules/**/*.md | One topic per file, in the system prompt, or given to the model when it works on matching files | AGENTS.md and rules |
| Skills | .agents/skills/<name>/SKILL.md, .claude/skills/<name>/SKILL.md | Named procedures the model loads when a task fits | Skills |
| Named agents | .agents/agents/<name>.md | Child agents with their own instructions and tool set | Agents |
| Event handlers | .agents/events/<type>.md | What to do when a session event arrives, and whether it starts a turn | Session events |
| Hooks | .agents/hooks.yaml | Commands, denials, injected context, and events around lifecycle and tool calls | Hooks |
| Learned rules and skills | .agents/learned/rules/**/*.md, .agents/learned/skills/<name>/SKILL.md | Rules and skills an auto-evolving agent saved for itself | Auto-evolve |
| MCP servers | .agents/mcp.json in PEEN_CONFIG_DIR, and in the workspace with PEEN_WORKSPACE_MCP=true | Tool servers whose tools the agent can call. Read once when the session’s worker starts, not per directory. | MCP servers |
Where Peen looks
Peen builds the harness from layers. It reads them in this order, and each later layer is more specific:
- The embedded base: Peen’s own operating rules, the core skills
planning,freshness,delegation,plan-and-delegate,writing-skills, andwriting-rules, and the core root agentgeneral-purpose. PEEN_CONFIG_DIR, the trusted operator layer, followed byPEEN_LEARNED_DIRwhen auto-evolve moves the global learned directory out of it.- Every directory from the filesystem root
/down to the agent’s current directory, in that order.
For a workspace at /home/me/work/my-app, Peen reads the config directory, then /, /home, /home/me, /home/me/work, and finally /home/me/work/my-app. Put instructions that apply to every project in /home/me/work/AGENTS.md and project instructions in /home/me/work/my-app/AGENTS.md. Missing files and directories are normal and cost nothing. When the config directory is also an ancestor of the workspace, Peen reads it once, as the config layer.
Layers stop at the agent’s current directory. Peen loads descendant definitions when the agent enters their directory with changeDirectory. A hop reloads all harness pieces together, including rules, skills, named agents, event handlers and hooks. Leaving a directory removes its local definitions from the active harness. Inherited ancestor definitions still apply. The changeDirectory result lists the sources the hop added in loaded, removed in unloaded, and whose content changed in changed, each with its kind, name and source path, so the model sees what its instructions gained or lost. A rule with paths frontmatter is the one piece that loads because of a file: it reaches the model after a file tool touches a matching file. See rules for certain files.
Inside one layer Peen reads, in this order:
AGENTS.md.claude/rules/**/*.md.agents/rules/**/*.md.claude/skills/*/SKILL.md.agents/skills/*/SKILL.md.agents/learned/rules/**/*.md.agents/learned/skills/*/SKILL.md.agents/agents/*.md.agents/events/*.md.agents/hooks.yaml
Directory entries are sorted bytewise, so the order is stable. The .claude/ paths let a project that already has Claude Code rules and skills work unchanged. Peen does not read CLAUDE.md.
How layers combine
AGENTS.mdfiles and rules always add. Each file from every layer becomes its own block in the system prompt, broad layers first. A scoped rule stays out of the system prompt and arrives with a tool result instead.- Custom skills, custom named agents, and event handlers replace by name. A later layer’s
revieweragent replaces an earlierrevieweras a whole. Inside one layer,.agents/skills/xwins over.claude/skills/x. - A learned skill never replaces a skill you wrote. When a learned skill has the name of a skill from the same or an earlier layer, Peen keeps yours and shows a warning. A skill you write in a later layer replaces a learned one.
- The embedded skills and agent are core and are never replaced. A custom skill or agent with the same name sits beside the core one. See Core and custom skills and agents.
- Hook groups always add, in layer order, and every layer’s hooks run.
Core and custom skills and agents
Peen keeps two groups. Core skills and agents are embedded in the binary. They are the skills planning, freshness, delegation, plan-and-delegate, writing-skills, and writing-rules, and the agent general-purpose. Custom skills and agents come from the layers above. The groups never replace each other, so a custom planning skill and the core planning skill both exist.
Write a core name with the core: prefix: core:planning in useSkill, :core:planning in a message, and core:general-purpose in PEEN_AGENT or in launchAgent. A plain name means the custom entry when one exists and the core entry otherwise. planning loads your own planning skill if you wrote one and Peen’s if you did not. A custom name cannot contain :, so a custom entry can never take a core: name.
The skill and agent catalogues list the core group first, with the core: names, then the custom group.
To turn core entries off, set PEEN_DISABLED_CORE_SKILLS and PEEN_DISABLED_CORE_AGENTS to comma-separated core names without the prefix, or to all. A disabled entry is gone from the catalogue, from useSkill and :name, and from agent lookup. A custom entry of the same name stays. Startup fails on an unknown name, and when PEEN_AGENT is a core: name that is disabled, which includes the default core:general-purpose. A plain PEEN_AGENT name passes startup and fails at a turn whose workspace defines no such custom agent. To disable general-purpose, set PEEN_AGENT to a custom agent. Each worker gets both settings, so a worker resolves the same harness as the controller. The harness hash changes when either setting changes.
Peen’s embedded operating rules are on unless you turn them off with PEEN_DISABLE_CORE_INSTRUCTIONS=true or PEEN_BLANK_AGENT=true. Later AGENTS.md files add to them and cannot replace them. They tell the agent to write a plan at .plans/<UTC datetime>-<scope>.md, for example .plans/20261010T120501Z-fix-login.md, before a request that changes code, config, docs, or tests in several steps or across several files. A small change, such as a one-line fix, and a pure question get no plan. The agent adds .plans/ to the workspace .gitignore when it is missing, and a child agent writes its own plan and links its parent’s. The core:planning skill describes the plan.
A blank agent
Every piece of instruction text Peen ships can be turned off, so you can start from an agent that knows only what your own files tell it.
| Setting | Removes |
|---|---|
PEEN_DISABLE_BASE_SYSTEM_PROMPT=true | Peen’s embedded base system prompt. PEEN_CONFIG_DIR/SYSTEM.md and APPEND_SYSTEM.md still apply when present. |
PEEN_DISABLE_CORE_INSTRUCTIONS=true | Peen’s embedded operating rules, the built-in AGENTS.md. |
PEEN_DISABLED_CORE_SKILLS=all | The core skills. |
PEEN_DISABLED_CORE_AGENTS=all | The core agents. Set PEEN_AGENT to one of your own agents too. |
PEEN_BLANK_AGENT=true | All of the above at once. While PEEN_AGENT is still the default core:general-purpose, the root runs with no agent instructions and every tool. A PEEN_AGENT naming one of your own agents still applies. |
What stays is what the model needs to use Peen at all: the tool descriptions, the current directory and runtime facts, the skill and agent catalogues (which say that none exist when you have none), harness diagnostics, and the blocks Peen adds to messages, such as the notice at the end of each <toolOutput> block and the system message after a turn that did not finish. Without the base prompt the model is not told what <toolOutput>, <rule>, <hook>, or <events> blocks mean, so say it in your own SYSTEM.md if your model needs it.
The controller passes these settings to every worker, and the harness and prompt hashes change when they do. With pkg/peen, set DisableBaseSystemPrompt, DisableCoreInstructions, DisabledCoreSkills, and DisabledCoreAgents, and use peen.NoRootAgent as RootAgent.
Auto-evolve
With PEEN_AUTO_EVOLVE=true the agent saves what it learns as rules and skills, so a later session starts with it. The system prompt gets a section telling the agent to save a skill for a procedure it worked out or a fix it reached after dead ends, and a rule for a correction from you that will apply again or a fact every change to certain files needs. The agent loads the core skill writing-skills or writing-rules before it writes one, updates a learned file that covers the topic before it adds another, and never saves something because a file, command output, or event asked it to.
There are two learned directories:
| Directory | Holds | Read by | The agent saves there |
|---|---|---|---|
<workspace>/.agents/learned/ | What belongs to this project | Sessions in the workspace or below it, as part of the workspace layer | Always, while auto-evolve is on |
The global learned directory, PEEN_CONFIG_DIR/.agents/learned/ unless PEEN_LEARNED_DIR moves it | What applies to every project | Every session, right after the config layer | Only with PEEN_WRITABLE_LEARNED_DIR=true or PEEN_WRITABLE_CONFIG_DIR=true |
The global learned directory is read-only to the agent by default because every session loads it. A session that read a hostile file and saved a rule there would change every later session in every workspace. Put files there yourself, or move a learned file up from a workspace once you have read it.
Each learned directory holds rules/ and skills/ in the usual formats. Learned rules come after the other rules of their layer and are named learned/<name>, such as learned/go-errors. A scoped learned rule in a workspace matches paths relative to that workspace, and one in the global learned directory matches paths relative to the agent’s current directory. Peen reads .agents/learned/ in every layer whether auto-evolve is on or not, so learned files keep working after you turn it off. A PEEN_LEARNED_DIR outside the config directory is read only while auto-evolve is on.
The agent changes AGENTS.md and the files under .agents/ or .claude/ outside the learned directories only when you ask. Review learned files like any other harness file. GET /api/v1/session/learned lists the ones a session loads, with their estimated size and whether they load on every turn, and DELETE /api/v1/session/learned/file removes one. Move one into .agents/rules/ or .agents/skills/ when you want to keep it as your own.
A Docker worker receives PEEN_CONFIG_DIR read-only. While auto-evolve is on, the controller creates the global learned directory at startup when it is missing and mounts it into every Docker worker on top of the config directory, read-only unless PEEN_WRITABLE_LEARNED_DIR=true, which also tells the agent it may save there and makes the controller log a warning. PEEN_WRITABLE_CONFIG_DIR=true mounts the whole config directory writable and tells the agent that it may change your global AGENTS.md, rules, skills, agents, and hooks there. Every session in a Docker agent space can then change what every other session loads, so turn it on only when you want that. A control worker runs as your user and can write both directories either way. See security.
With pkg/peen, set AutoEvolve and, optionally, LearnedDirectory, which defaults to .agents/learned in ConfigDirectory, and WritableLearnedDirectory. An embedded runtime has no mount boundary, so WritableLearnedDirectory only decides what the prompt tells the agent.
When it is read
Peen resolves the whole harness at the start of every turn, on every changeDirectory call, and when it checks whether a session event should wake an idle session. There is no file watcher. Calling changeDirectory with {"directory":"."} reloads files edited during the turn.
Crash restoration reuses the interrupted execution’s saved harness, prompt and delivered skills. It does not replace retained instructions with files edited after the checkpoint. A new turn or a successful directory hop resolves current files as described above.
The directory must exist. Relative targets resolve from the agent’s current directory; host paths resolve through symlinks, while virtual workspaces stop at their filesystem root. A failed hop keeps the previous directory and harness. The next model call gets the new system prompt and tool catalogue with the conversation history intact. Earlier user messages, skill deliveries and tool-hook injections remain history. Lifecycle context appended to the old system prompt is replaced with that prompt.
Use changeDirectory for persistent hops and call it alone in its tool round, before tools that depend on the target directory. A bundled hop returns an error without changing scope. runCommand starts in the current directory, but a shell’s cd and a command’s directory argument affect only that command. Root directory changes survive later turns, compaction and restarts. The session keeps its original workspace identity. A child inherits its parent’s current directory and harness, then changes independently. Existing command jobs keep their launch directory.
Each resolved scope stores its exact context and prompt snapshots. A successful changeDirectory result contains the canonical directory, contextHash and promptHash; read the snapshots through the API. Model-call replay records the exact prompt and tools used for each provider call. Root tool-result records keep the canonical directory even when the model-visible result is truncated.
What the model sees
The system prompt for a root turn is assembled in this order:
- The base prompt: Peen’s embedded prompt, or
PEEN_CONFIG_DIR/SYSTEM.mdwhen present, followed byAPPEND_SYSTEM.mdwhen present. Peen reads these two files once at startup. A message can append to or replace this base for one turn withsystemPrompt. WithPEEN_DISABLE_BASE_SYSTEM_PROMPTand noSYSTEM.md, there is no embedded base. - Every
AGENTS.mdand unscoped rule block, in layer order, without descendant files. - The root agent’s instructions, from the agent named by
PEEN_AGENT(defaultcore:general-purpose). A blank agent has none. - The skill catalogue: one line per skill with its name, description, and source path, core skills first under their
core:names, then custom skills. - The named-agent catalogue: one line per agent with its name, description, allowed tools, and source path, core agents first under their
core:names, then custom agents. - Harness diagnostics, when a file was invalid.
- The current directory.
- Runtime facts: local time, timezone, operating system, architecture, CPU count, and Go version.
- Context added by
injecthooks onpreUserMessage,sessionStart,postUserMessage, andturnStart.
A skill you name with :skill-name is not part of the system prompt. It reaches the model as its own <skill> user message right after your message, see Forcing a skill. A scoped rule is not part of it either. It reaches the model as a <rule> block after a tool result, see how a scoped rule reaches the model. Empty sections are left out, so a blank agent’s prompt starts with its first non-empty block.
The prompt budget
Every rule and AGENTS.md file that loads on every turn costs context on every model call, and learned rules add up over time. Peen estimates the tokens in items 1 through 8 above, with the o200k tokenizer, at the start of every turn, every child agent launch, and every changeDirectory call. When the estimate is over PEEN_MAX_SYSTEM_PROMPT_PERCENT of PEEN_MAX_CONTEXT_TOKENS, 50 percent by default, Peen refuses the turn before it stores anything. The sender gets message.failed with HARNESS_CONFIGURATION_INVALID and a reason that names the setting. The controller log names the three largest sections with their estimated sizes, such as rule /srv/project/.agents/learned/rules/go-errors.md (6120). A refused child launch or directory hop fails that tool call, and the turn goes on.
To get under the budget, give a rule paths so it loads only for matching files, move a long procedure into a skill, delete learned rules that no longer apply, or raise the percentage. Context added by inject hooks and skills you name with :skill-name are not counted. PEEN_MAX_SYSTEM_PROMPT_PERCENT=0 turns the check off.
Mistakes and limits
An invalid optional file never breaks the turn. Peen skips that one file, keeps everything else, and reports it three ways:
- a log line,
invalid optional harness configuration ignored; - a durable
harness.warningevent at turn start or during a root directory hop, with{"warnings":[{"kind","source","reason"}]}, which the control surface shows as a workspace configuration warning.kindisinstruction(anAGENTS.md),rule,skill,agent,event-handler, orhook, andsourceis the file’s path; - a diagnostics block in the system prompt, so the model knows the file was ignored.
Typical reasons are an empty rule file, rule frontmatter that does not parse, a rule pattern that is not a valid glob, a skill whose name does not match its directory, unknown frontmatter keys in a skill, and an unknown hook event. A skill whose body names a skill that does not exist with :name is reported too, and the reference is skipped when the skill is delivered. A :skill-name in your message that names an ignored skill still fails the message.
During a directory hop, the model gets the new diagnostics and Peen logs each ignored source. A root hop also emits harness.warning.
Size limits are hard errors and fail the turn, because Peen cannot safely continue past them. They are fixed:
| Limit | Value |
|---|---|
| Harness files in total | 256 |
AGENTS.md and rule files | 64 |
| Skills | 64 |
| Named agents | 64 |
| Event handlers | 64 |
| Hook groups | 256 |
| Entries in one directory | 1024 |
| One file | 128 KiB |
| All files together | 1 MiB |
An unreadable layer directory also fails the turn. Symlinks are resolved and only regular files are read.