Keeper agents
Every chat in Paddock is run by a Claude Code agent registered with herdctl’s
FleetManager. There is one kind you interact with — the keeper, one per
project — plus the sweeper, which is an internal per-project
agent you never chat with directly.
This page used to be “Keeper vs. scratch agents”. Paddock had a second, shared
scratchagent for one-off chats that belonged to no project. #516 retired it: the root of the instance is now a project like any other, so a chat that belongs to no particular project is simply a chat of the root project, run by an ordinary keeper. Every capability scratch was deliberately denied — the self-management MCP, curation, triggers, attachments, run history, a CLAUDE.md that actually reaches it, more than one turn at a time — a root chat has for free.
Keeper — one per project
Section titled “Keeper — one per project”A keeper is the long-lived agent that owns a project. It is
registered as keeper-<slug>, and its working directory is the project’s
workingDir — the project dir for a notebook project, or the nested checkout for
a repo-backed one. Because Claude Code keys transcripts by working directory, the
keeper’s cwd is what ties a project’s chats to that project.
- Registered programmatically at startup and on project create/update via
HerdctlService.ensureProjectAgent()(fleet.addAgent(config, { replace: true })— no yaml round-trip). SeekeeperAgentConfig()inherdctl.ts. - Runs the project’s default model (
project.model ?? KEEPER_DEFAULT_MODEL, Opus by default) and honors the project’spermissionMode,maxTurns, anddriveMode. - Allows up to
KEEPER_MAX_CONCURRENT(10) concurrent chats, so several chats — and forked children — of the same project can run in parallel. - Can receive the self-management MCP tools (env-gated).
Because a keeper is one shared agent per project, a per-chat model override is
applied by re-registering the keeper (ensureKeeperModel) — last-write-wins
across concurrent chats of the same project. Acceptable for single-user; a clean
per-trigger override is a herdctl follow-up.
The root keeper
Section titled “The root keeper”The root project’s keeper is keeper-__root, and its working directory is
projectsRoot — the directory that contains every project. It is an ordinary
keeper in every mechanical respect, but worth calling out plainly: its cwd
contains every project, so a root chat can read and edit any project’s files, and
root’s git status is the whole backing repo. That is the intent — the root is
where you act across the instance — but it is a real escalation over a project
keeper, which is confined to its own subtree.
Its chats live at /chat and in <projectsRoot>/.chats/.
Promotion: giving a chat its own project
Section titled “Promotion: giving a chat its own project”A chat that turns out to matter can be promoted into a project of its own,
re-homing it under that project’s keeper.
HerdctlService.promoteSession(sessionId, from, to) (herdctl.ts, wired at
POST /api/projects/:slug/chats/:sessionId/promote):
- Moves the transcript from the source project’s
.chats/into the new project’s.chats/, preserving mtime. - Rewrites the embedded
cwdtoken in the JSONL to the new project’sworkingDir— the checkout, for a repo-backed project. (Resume does not depend on this: Claude Code keys resume on where the transcript is, not on its recordedcwd. The rewrite keeps the file honest about itself.) - Evicts the source agent’s in-process session state
(
deleteSession(keeper-<from>, sessionId)) so a same-process resume works. - Re-attributes the session to
keeper-<to>and invalidates both agents’ discovery caches so the chat immediately shows under the new project.
The UI offers this on root chats — the ones that belong nowhere in particular, which is exactly the population promotion was invented for. The server route is generic.
A related operation, forkSession, copies a session (minting a new session id)
rather than moving it — see Chats.