# Guide

Coding Owl runs coding agents on your machine while it is otherwise idle, so
that work you queued up happens while you are away and is waiting for review
when you come back.

You describe a piece of work, Owl queues it against one of your repositories,
and once your machine has been idle for a while it hands the work to an Agent
(Claude Code today) in a git worktree and branch of its own. When you sit back
down, Owl freezes the Agent and gives the machine back. When the work is done
and passes your project's checks, it lands in a branch for you to review.

## How it works

- **Jobs are queued, not run.** `owl add` records a standing intent to do one
  piece of work in one Project. The queue is first in, first out, and can be
  reordered.
- **Idle is the trigger.** By default Owl works only after ten minutes without
  keyboard or mouse input and with the machine on AC power. Both are
  configurable, and `owl start` and `owl pause` override them in either
  direction.
- **Every Job gets its own worktree and branch.** An Agent never touches your
  checkout. The branch is rebased onto the Project's base branch at the start of
  every Run.
- **Plan first, then execute.** A Job's first Run produces a plan, which becomes
  the execution prompt. Every Run starts in a fresh context and orients from a
  handoff document committed on the Job's branch, so a Job can take several
  Runs without any conversation surviving between them.
- **Giving the machine back is instant.** On your first input event Owl stops
  the Agent's whole process group. If the machine goes idle again within the
  grace window the same Run continues. Otherwise the Run ends and the next one
  picks up from the handoff.
- **Verification gates completion.** After the Agent exits, Owl runs the
  Project's checks from the base branch, out of the Agent's reach. A Job that
  passes goes to review. One that fails is blocked and says why.
- **You have the last word.** `owl jobs accept` keeps the work and reclaims the
  worktree. `owl jobs drop` deletes the branch and the worktree.

Owl is built around a small vocabulary - Project, Job, Run, Handoff, Idle,
Verification, Account, Driver, Executor - defined in [CONTEXT.md](https://github.com/vojtechmares/coding-owl/blob/main/CONTEXT.md).
Every decision behind the design is recorded in [docs/adr](/docs/decisions).

## Status

Owl is early software, versioned as v0.x.y, and currently ships for macOS on
Apple silicon only. Idle detection is implemented for macOS. Read the risks
below before you queue work.

## Risks

Owl runs an Agent on your machine while nobody is watching. Treat a Job the way
you would treat running the same tool yourself, unattended:

- **There is no sandbox.** Agents run as ordinary processes under your user,
  not in a container (ADR-0006). The worktree is where an Agent works, not a
  wall around it: an Agent can read and write anything you can, use the
  network, and reach whatever your user is signed in to, git remotes included.
- **The default allowlist runs code.** A new Account's Agents may edit files
  and run `git`, `make`, `go`, `npm`, `pnpm`, `yarn`, `npx`, `cargo` and
  `pytest` without asking. Build tools and package runners execute whatever a
  repository or a package tells them to, and `git` includes `git push`. The list
  is `accounts/<name>/settings.json` under Owl's data directory; narrow it if
  that is more than you want (ADR-0035).
- **Verification runs the Agent's code.** The checks come from the base branch,
  but they run against the Agent's changes, so a test the Agent wrote is a test
  that runs.
- **Time is capped by default; spend is not.** A Run ends when the Agent exits,
  when you come back to the machine, or when it passes one of its phase's
  limits - four hours of work or fifteen minutes of silence, by default, and
  configurable per phase. Spend is a separate question: `budgetUSD` in a
  Project's configuration caps what one Run may spend and an Account's `limits`
  keep Owl from starting work past a share of your subscription, but neither is
  set until you set it.
- **The desktop app is not notarised.** It is ad-hoc signed, and the cask
  clears macOS's quarantine flag so that Gatekeeper opens it. You are trusting
  the GitHub release rather than Apple's check.

## Installing

Owl ships as a Homebrew formula. Install it and keep the daemon running across
logins:

```
brew install vojtechmares/tap/coding-owl
brew services start coding-owl
```

The desktop app is a separate cask that depends on the formula:

```
brew install --cask vojtechmares/tap/coding-owl-desktop
```

On a machine without Homebrew, `owl daemon install` writes a launchd agent
that keeps `owl daemon run` running. The daemon never daemonizes itself; that
is launchd's job.

Then run:

```
owl setup
```

It checks the three things Owl runs nothing without - a daemon it can reach,
a `claude` the daemon will find, and an Account to run on - says which are
already done, and asks only about the rest. It starts no daemon by itself:
with Homebrew it names the `brew services` command above, and elsewhere on
macOS it offers to write the launch agent. What it does about `claude` is the
section below, and what it does about an Account is
[Getting started](/docs/getting-started)'s first step - both worth
reading when you would rather do it by hand.

### Finding Claude Code

The daemon runs Claude Code on your behalf, so `claude` has to be somewhere the
daemon can find it. A daemon started from your shell has your `PATH`. A daemon
kept running by `brew services` does not: it gets a fixed `PATH` of Homebrew's
own directories plus `~/.local/bin`, which is where Claude Code's installer
puts `claude`. If yours is somewhere else, name it in the daemon's own file,
`config.yaml` in Owl's configuration directory:

```yaml
apiVersion: codingowl.dev/v1
claudePath: /path/to/claude
```

When `claudePath` is set the daemon uses it and does not look on `PATH`. When
it is not, the daemon looks on `PATH` and then under `~/.local/bin`. The daemon
reads the setting when it starts, so after changing it run
`brew services restart coding-owl`.

`owl setup` does this for you: it looks in exactly those places, and when it
finds nothing it asks where `claude` is and writes that one setting into the
file, leaving everything else in it as it was.

## Quick start

Adding an Account, registering a Project, telling Owl how to verify the work,
queueing a Job, letting it run and reviewing it in the morning, with the
commands for each: [Getting started](/docs/getting-started).

## Configuration

Every config file carries `apiVersion: codingowl.dev/v1`. Owl refuses a file
whose version or keys it does not recognise rather than guessing.

Both file kinds have a JSON schema, generated from the same structs Owl
decodes them with, so an editor can complete the keys and underline a
misspelled one where you type it rather than leaving it for `owl project show`
to refuse. Point your editor at one with a comment on the first line - VS Code,
Neovim and the JetBrains editors all read it through the YAML language server:

```yaml
# yaml-language-server: $schema=https://codingowl.dev/schema/project.json
```

```yaml
# yaml-language-server: $schema=https://codingowl.dev/schema/daemon.json
```

### Project configuration

Discovered in this order, first match wins:

1. `<project>/.coding-owl.yaml`
2. `<project>/.config/coding-owl.yaml`
3. `<project>/.meta/coding-owl.yaml`
4. `~/.config/coding-owl/<project-name>/config.yaml`

The in-repo forms are read from the Project's base branch, never from a Job's
worktree, so an Agent cannot weaken the checks that judge it. The fourth form
is for Projects that should carry no Owl file, and for overrides you do not
want committed. `owl project show` prints which file is in force.

```yaml
apiVersion: codingowl.dev/v1
account: work            # the Account this Project's Jobs run on
branchPrefix: owl/       # the default; Job branches are named under it
setup:                   # prepares each fresh worktree before an Agent starts
  - pnpm install --frozen-lockfile
checks:                  # shell commands; all run, every failure is reported
  - name: build
    run: go build ./...
  - name: test
    run: go test ./...
    timeout: 10m
  - name: fmt
    run: gofmt -l .
    expect: empty_output # default expectation is exit zero
verification:
  agent: true            # additionally have a fresh Agent review the diff
phases:                  # model, effort and limits per phase
  plan:
    model: anthropic/claude-opus
    effort: xhigh
    timeout: 1h          # the longest this phase's Agent may run at all
    stall: 15m           # the longest it may go without saying anything
  execute:
    model: anthropic/claude-sonnet
    timeout: 4h
allowedTools:            # what an unattended Agent may do without asking
  - Bash(go test:*)
skills:                  # reusable instructions fetched and pinned by Owl
  - git: owner/repo
    ref: main
unattendedClauses:       # appended to Owl's standing unattended contract
  - Never touch the migrations directory.
```

### Daemon configuration

`~/.config/coding-owl/config.yaml` holds global settings:

```yaml
apiVersion: codingowl.dev/v1
claudePath: /opt/homebrew/bin/claude
credentialStore: keychain
idle:
  after: 10m
  interval: 30s
  requirePower: true
graceWindow: 15m
maxParallelRuns: 2
accounts:
  work:
    limits:
      fiveHourMax: 60
      weeklyMax: 50
garbageCollection:
  interval: 1h
  reviewAfter: 168h
```

Account limits are percentages of a subscription's rate-limit window. Owl
will not schedule past them, measured against the account's total usage rather
than Owl's alone, so there is always room left for you.

Most of these are read again as they are used, so an edit takes effect on the
next scheduling decision rather than on the next restart: the idle policy, the
grace window, `maxParallelRuns`, the account limits, and both
`garbageCollection` settings. A change to `garbageCollection.interval` counts
from the end of the wait already under way, which for a daemon that has just
been told to collect once a day means a day.

Three are settled when the daemon starts and wait for a restart:
`credentialStore` and `claudePath`, so that a Run already going cannot have
its credential store or its tool change under it, and `idle.interval` - how
often this daemon looks at the machine, as against `idle.after`, which is what
it holds the machine to and is read every look.

### Account configuration

An Account's configuration directory is its coding tool's configuration
directory, so the tool's own commands are how that Account is configured. `owl
account exec` runs the tool against one:

```
owl account exec work -- mcp add --scope user sentry --transport http https://mcp.sentry.dev/mcp
owl account exec work -- plugin install some-plugin
owl account exec work -- mcp list
```

Everything after `--` is the tool's, passed through untouched, and the tool's
exit status is the command's. Owl models no MCP server and no plugin: it points
the tool at the right Account and gets out of the way.

Two things to know about an MCP server an Agent is meant to use. An unattended
Agent denies anything not on its allowlist, so the server's tools have to be
granted as `mcp__<server>__*` in the Account's `settings.json` or in a
Project's `allowedTools`. And a repository's own `.mcp.json` servers wait for
an approval nobody is awake to give, so user scope - what `--scope user` writes,
in the Account's own directory - is the one that works for a Run.

### Standing instructions

An Account carries standing instructions that every Run on it reads, whichever
Project the Run is for. They are the place for conventions that hold across all
of an Account's work, as against a Project's `unattendedClauses`, which hold
only for that Project.

```
owl account instructions edit work            # in $VISUAL, $EDITOR, or vi
owl account instructions set work < CLAUDE.md # from a file
owl account instructions show work
```

They are also editable in the desktop app. Owl keeps them in the Account's
configuration directory under the name that Account's tool reads instructions
from - `CLAUDE.md` for Claude Code - so the tool reads them itself and Owl
injects nothing. Saving blank text takes them away.

One file changes every Job on that Account, in every Project, so it is worth
keeping short. To read them back as an Agent on that Account would, ask it:

```
owl account exec work -- --print "what standing instructions are you under?"
```

### Filesystem layout

Owl honours the XDG variables on both macOS and Linux:

```
~/.config/coding-owl/        daemon config and per-Project fallback config
~/.local/share/coding-owl/   owl.db, worktrees, per-Account tool config, skills
~/.local/state/coding-owl/   daemon socket and per-Run logs
```

## Command overview

| Command | What it does |
| --- | --- |
| `owl setup` | Walk through what Owl needs before it can run anything |
| `owl project add\|list\|show\|rename\|move\|remove` | Register repositories as Projects |
| `owl project setup [project]` | Give a Project the Account its Jobs run on |
| `owl account add\|list\|remove\|exec` | Manage the subscriptions Owl runs work on |
| `owl account instructions show\|set\|edit` | The standing instructions every Run on an Account reads |
| `owl add <prompt>` | Queue a Job, planned first unless `--no-plan` |
| `owl queue list\|reorder\|remove` | Show and reorder the queued Jobs |
| `owl start` / `owl pause` / `owl resume` | Override idle: run now, freeze everything, continue |
| `owl status` | What ran, what is running, what is waiting for you |
| `owl jobs show\|accept\|drop\|extend` | Inspect a Job, keep or refuse its work, give it more Runs |
| `owl jobs label add\|remove` | Change the labels a Job carries |
| `owl logs <run> [-f]` | The Agent's own structured stream, one event per line |
| `owl skills add\|list\|update\|remove` | Manage the Skills a Project gives its Agents |
| `owl models` | The models a Job's phases can run on, aliases and pinned |
| `owl providers add\|list\|remove\|supported` | Model providers the desktop chat speaks to, and the ones it can be configured with |
| `owl gc` | Reclaim finished worktrees, report what is unfinished |
| `owl daemon run\|install\|status` | Run, install and inspect the daemon |

Every command talks to the daemon over ConnectRPC on a unix socket. The CLI
and the desktop app are both clients; the daemon is the single source of truth.

For when to reach for which of them, the manual is in two pages:
[Working with Jobs](/docs/jobs) and
[Projects and Accounts](/docs/projects-and-accounts).

## Desktop app

The desktop app is a Wails application with a React frontend. It is a pure
view of the daemon: overview, queue, Jobs with their Runs, diffs, handoffs,
Verification results and logs. Everything it can do exists as a daemon RPC the
CLI can also call.

It also carries a chat, answered by the daemon. The chat proposes commands and
runs nothing without consent, and what it can run is a fixed allowlist of
read-only programs executed directly, without a shell, confined to a Project or
one of its worktrees. Which model answers it is configured with `owl providers`:
see [Chat model providers](/docs/chat-providers).

Screenshots live in [docs/desktop](https://github.com/vojtechmares/coding-owl/blob/main/docs/desktop).

## What Owl promises an unattended run

- The Agent's working directory is its Job's worktree, and the Job's branch is
  the only branch it works on.
- Verification config comes from the base branch, so the Agent being judged
  cannot change the rules.
- Every Run carries a standing unattended contract: make reasonable assumptions
  and write them down, commit incrementally, keep the handoff current, stop and
  say so when genuinely blocked, never guess at anything destructive.
  `owl jobs show` prints the effective system prompt so nothing is hidden.
- An Agent that ignores a request to stop is killed.
- Every Run ends. A phase's Agent is bounded both by how long it may run at all
  and by how long it may go without saying anything, so one that wedges on an
  untouched machine cannot hold the queue until morning. Passing either limit
  is the Job's own failure and spends one of its attempts, so a Job that does
  it every time is reported rather than retried for ever.
- Garbage collection never deletes uncommitted work. Anything that looks
  unfinished is reported under `owl status` for you to decide.

## Development

Owl is a single Go module. The CLI, the daemon and the desktop backend all live
in it; plugins (Drivers, Executors, Verifiers) are Go interfaces, not external
processes.

```
make build      # bin/owl
make test       # go test ./...
make lint       # go vet, gofmt, buf lint
make generate   # regenerate the ConnectRPC code from proto/
make desktop    # the Wails app; needs the Wails CLI, Node and pnpm
```

Behaviour tests in `tests/behavior` drive the built binary against a fake
Claude Code and a fake machine, one scenario file per issue. Releases are cut
by `scripts/release.sh`, which tags `main`; the release workflow builds the
tag, publishes the assets and updates the Homebrew tap.

Layout:

```
cmd/owl           the owl binary: CLI and daemon
cmd/owl-desktop   the Wails desktop app
internal/         daemon, queue, run, drivers, executor, verifier, gc, ...
proto/, gen/      ConnectRPC service definitions and generated code
deploy/launchd    the launchd agent template
docs/adr          architecture decision records
docs/agents       how AI agents work on this repository
docs/guide        getting started and the CLI manual
tests/behavior    end-to-end behaviour scenarios
```

Working on the repository with an agent? Start with [AGENTS.md](https://github.com/vojtechmares/coding-owl/blob/main/AGENTS.md).

---

Source: [README.md](https://github.com/vojtechmares/coding-owl/blob/main/README.md)
