peen: A Coding Agent Server That Does Not Die With Your Terminal

Peen is a self-hosted coding agent server. You point it at a project directory, give it a model and talk to it from a browser or terminal. It can read and edit the project, run commands, follow the repository’s own rules, split work among child agents and keep the entire session after you close the client or restart the server.

The important word is server. Peen is not a browser chat with a shell bolted onto it. The browser is a control surface. The command line is another client. The durable session belongs to the workspace, and the work keeps running in a supervised worker whether a particular client remains connected or not.

I built it because I wanted to start a coding job in one place, walk away, return from somewhere else and inspect the same work instead of reconstructing a dead terminal session from logs. I also wanted project rules, tools, child agents, running processes and crash recovery to be parts of one system rather than a pile of wrappers that only agree while everything is healthy, which is when even the shittiest architecture looks convincing.

Install One Server

On Linux, the normal installation is one command:

curl -fsSL https://raw.githubusercontent.com/psyb0t/peen/main/install.sh | bash

That installs the peen command under ~/.local/bin and creates its configuration under ~/.config/peen. The default setup runs the controller in Docker and gives every workspace agent its own Docker container. The controller owns the sessions and APIs. A worker gets the project it is supposed to work on, not an invitation to rummage through the rest of the machine like a raccoon with shell access.

Peen needs a model provider before it can do anything useful:

peen config edit

The configuration supports OpenAI-compatible endpoints, Anthropic and Z.ai Coding. A provider receives a local name, an endpoint and the name of the environment variable that contains its key. Models then use explicit provider/model names. A small OpenAI-compatible setup looks like this:

PEEN_UPSTREAMS=[{"name":"gateway","type":"openai","baseUrl":"https://gateway.example/v1","apiKeyEnv":"GATEWAY_TOKEN"}]
PEEN_DEFAULT_MODEL=gateway/your-model-id
PEEN_COMPACTION_MODEL=gateway/your-model-id
GATEWAY_TOKEN=your-token-here
PEEN_API_TOKEN=

The JSON is deliberately unquoted because the file is Docker --env-file input, not a shell script. The key stays in the environment file and the provider definition only names it. peen doctor checks the installation and configuration before the first real job, which is much nicer than discovering a misspelled variable after a model call should already have started.

Open a Project and Start Working

Move into a project and run Peen:

cd ~/work/my-app
peen

Peen starts the controller if it is not already running, opens the workspace session for that directory and gives you a browser URL. Type a job into the page and the reply streams beside the tool calls, command output, reasoning summaries and child-agent activity. Running peen from the same directory later returns to the same session instead of dumping you into another blank fucking chat with no idea what happened before.

You can skip the browser and run one turn directly from the terminal:

peen --prompt "find the failing test, fix it and run the relevant suite"
peen --model gateway/another-model --output text --prompt "review the current changes"
printf '%s\n' 'explain this project' | peen --prompt -

The terminal and browser are not separate modes with separate memories. Both talk to the same workspace session. If a turn is already running, another message is queued for the next safe point. If you want to watch from another terminal without starting work, peen session attach follows the stored event stream. peen session list shows the sessions, and peen session stop pauses one without deleting its history.

The Agent Works Inside the Project

A Peen agent has file tools, command tools, supervised background jobs, skills, child agents, messaging and recovery tools. The current server ships twenty-five built-in tools, but the number is less important than the way they behave together.

The agent can list and search files, read them, write them, apply patches, move paths and change directories. It must read an existing file before replacing it. Writes carry the hash of the content it observed, so a stale edit fails instead of silently crushing a newer change. That matters when the user, another agent or a formatter changes the same file while a long turn is still running.

Commands are supervised jobs rather than anonymous subprocess calls. The agent can start a build, read bounded output, wait again, send a signal or leave it running while it investigates something else. A wait ending does not mean the process is dead, and a reconnect does not cause the command to run twice. The job and its output remain attached to the session that started it.

This makes an ordinary request feel ordinary. Tell Peen to fix a failing test and it can inspect the repository, read the relevant code, run the failure, patch the implementation and verify the result. You do not have to turn every task into a rigid YAML ritual before the agent can touch the damn thing.

The Repository Supplies the Instructions

Put an AGENTS.md in the project root and Peen reads it when the session starts. That file is the place for the commands, boundaries and conventions the agent must know on every turn. More specific instructions can live under .agents/rules/ or .claude/rules/. Skills, named agents, event handlers and hooks live beside them.

my-app/
  AGENTS.md
  .agents/
    rules/
    skills/
    agents/
    events/
    hooks.yaml

The instructions follow the filesystem. A rule in ~/work/AGENTS.md can cover every project below that directory. A closer file adds or overrides instructions for one project or subdirectory. When the agent uses changeDirectory, Peen reloads the harness for the destination, so entering a nested Go module can load its Go rules and leaving it can drop them again.

File-scoped rules can declare paths, globs or applyTo. A rule about database migrations does not need to burn context while the agent edits CSS. It arrives when a matching file is first touched and can be restored after compaction. Skills work the same way at a larger scale: the model sees their names and descriptions, then loads the full procedure only when the task needs it.

Hooks handle the mechanical rules that should not depend on the model remembering a paragraph. A hook can deny a tool call, inject instructions, run a command or emit an event. That is useful for blocking a protected path, requiring a check after a change or waking a session when an outside system reports something worth handling. Trusted hooks belong in a read-only configuration layer, because a hook the agent may rewrite is security theatre with extra fucking steps.

Project files are not automatically trusted instructions. A source file, command output or fetched web page can contain text that tells the model to ignore the user and do something else. Peen wraps tool results in a <toolOutput> block that identifies the tool and source, escapes planted closing tags and tells the model that the contents are data rather than a new user request. This is not magical prompt-injection immunity, but it removes the ambiguity between something the operator asked and something an untrusted file happened to say.

Let It Keep Useful Lessons, If You Want

Peen can optionally turn repeated corrections into reusable project knowledge. Set PEEN_AUTO_EVOLVE=true and the agent gains tools for saving a procedure as a learned skill or a recurring rule as a learned rule. Workspace lessons go under .agents/learned/, including corrections under .agents/learned/rules/. A configured global learned directory can carry a useful lesson across projects, but Docker workers receive that global directory read-only by default. Set PEEN_WRITABLE_LEARNED_DIR=true, or the broader PEEN_WRITABLE_CONFIG_DIR=true, only when you deliberately want an agent to change knowledge shared by every workspace.

It is off by default for a good reason. Persistent learning can also make a bad instruction persistent. If hostile repository text persuades the agent to save a poisoned rule, later turns may inherit it long after the original file is gone. Learned files are plain Markdown, and the API now exposes GET /api/v1/session/learned plus GET and DELETE /api/v1/session/learned/file so a client can inspect or remove exactly what a session loads.

The harness also has a prompt budget now. By default, the complete system prompt may consume at most 50 percent of PEEN_MAX_CONTEXT_TOKENS. PEEN_MAX_SYSTEM_PROMPT_PERCENT changes that share, and 0 disables the check. An oversized prompt fails with HARNESS_CONFIGURATION_INVALID and the controller logs its three largest sections instead of letting rules quietly eat the room needed for the actual job.

Split a Job Without Opening Five Terminals

A workspace agent can launch child agents for bounded pieces of work. A child receives its own instructions, model settings, tool allowance and execution state. It can send results back to its parent, wait for more work or launch its own children when the job genuinely has another layer.

This is useful when the task has independent parts. One child can trace an API, another can inspect the tests and a third can read an external specification. The parent keeps the overall task, receives their messages and decides what belongs in the final change. You still have one workspace session instead of five unrelated transcripts and a human stuck gluing the shit together.

The child tree is durable too. Peen stores identities, ancestry, current directories, instructions, remaining budgets, loaded skills, inboxes, summaries and completed results. A child finishing after its parent process disappears does not become an orphaned line in a log file. Its retained state is available when the workflow continues.

The Session Does Not Belong to the Client

Peen owns the session and runs the agent. A browser, CLI process, bot or custom API client can attach, submit work, watch it and disconnect. None of them hosts the execution. Losing the client connection does not cancel the worker, kill its commands, pause its child agents or throw away the turn.

Connect again from the same client or a different one and it opens the same workspace session. Peen replays the persisted events it missed, including replies, tool calls, command output and child-agent activity. The client renders server-owned history instead of pretending that whatever shit survived in its local memory is the truth.

Only an explicit stop pauses unfinished work. Unattended sessions can keep calling the model and spending provider budget, so a client disappearing is deliberately different from telling Peen to stop. Reconnecting only shows what happened. It does not secretly resume a session that was explicitly paused.

A Restart Does Not Erase the Work

Peen stores conversations, model rounds, prompts, context snapshots, compactions, tool calls, tool results, process jobs, child-agent events, usage and cost records in SQLite. peen.db holds the control-plane state. workflows.db holds durable root, child and grandchild executions. Provider credentials are not stored in either database.

The workflow records input before an effect and records the result before acknowledging it. If a completed model request or tool call was saved before the crash, recovery can reuse it. If the process died while an effect was still in flight, Peen marks that activity uncertain instead of guessing that it failed and running it again.

That uncertainty is important. Repeating a file read is usually harmless. Repeating a deployment, API mutation, process signal or destructive command can make the recovery worse than the crash. The agent can search retained executions, inspect the uncertain activity, resolve what happened and resume from the known state. Recovery means continuing the workflow, not replaying every side effect and praying the repository survives the bullshit.

Compaction does not replace the stored transcript either. When the model context fills, Peen can summarize older rounds while preserving the opening and newest work. The compacted view is what the model needs for the next call. The full record remains in SQLite for the operator and recovery machinery.

Choose Where the Worker Runs

Peen separates the controller from the place where an agent executes tools. A control worker runs beside the controller. A docker:<profile> worker runs in its own container with the workspace and the mounts, network and capabilities chosen by the operator.

The default installation uses Docker workers because the execution environment is the real permission boundary. Peen does not put a decorative approval dialog in front of runCommand and call the problem solved. If the agent can run a command as your host user, it has that user’s access. If it receives the Docker socket, it effectively has host root. Peen refuses unsafe combinations instead of quietly falling back to a broader environment when Docker is unavailable.

A normal Docker worker does not receive the controller database, API token or controller socket. The project directory and selected configuration are mounted deliberately. Different Docker profiles can give different projects different tools or networks without letting a client invent mounts and privileges in an API request.

Tool output and transcripts can contain sensitive data, so the server still needs sane deployment. Set PEEN_API_TOKEN before exposing it to a network you do not fully trust, use TLS outside local development and mount only what the agent needs. Peen strips its provider and API variables from command environments, but it cannot make an exposed project secret cease to be a secret.

Use the Same Session From Other Software

The built-in browser is not a special path through the system. Peen exposes an OpenAPI-described REST API for stored state and control, plus an AsyncAPI-described WebSocket protocol for turns and live events. Another client can open a workspace, send a turn, follow the event sequence, reconnect and catch up without scraping the web interface.

The server uses gen-asyncapi for generated socket types and aichteeteapee for HTTP work. Outgoing frames are checked against the protocol contract, and public Go code under pkg/peen exposes the engine for applications that want Peen inside their own process.

There is also a GitHub Action for one coding turn on a Linux runner. It works in a checkout, leaves file changes for later workflow steps and returns the reply and result file. The same engine can therefore handle an interactive project session, a terminal request, a service integration or a CI job without each client inventing its own agent loop.

- uses: psyb0t/peen@main
  id: peen
  with:
    prompt: Inspect this project and write SUMMARY.md.
    working-directory: .
    model: gateway/your-model-id
    upstreams: '[{"name":"gateway","type":"openai","baseUrl":"https://gateway.example/v1","apiKeyEnv":"PROVIDER_KEY"}]'
  env:
    PROVIDER_KEY: ${{ secrets.PROVIDER_KEY }}

Operators also get Prometheus metrics on a separate loopback-only listener. The controller reports HTTP, database, model, workflow and tool activity without mixing the metrics endpoint into the public API. It refuses wildcard and non-loopback metrics addresses, so collecting local telemetry does not quietly publish an unauthenticated operational endpoint to the network.

What Peen Is For

Peen is for work that should belong to a project instead of a terminal tab. It gives one directory one durable agent session, lets the repository define how that agent behaves and keeps clients separate from the execution they observe.

If you only want to send one prompt to one model and print the reply, there are much smaller programs for that. Peen becomes useful when the agent must use real tools, supervise long commands, delegate work, survive disconnects, recover after failure and remain available through more than one interface without a stack of brittle wrapper shit pretending to be a system.

No client owns the work. The project session does.

Install Peen from github.com/psyb0t/peen, point it at a project and give the bastard some work.