# Flock API reference

> 0.7.21-alpha. The `flk` CLI, the MCP tools, the hook/event contract, and the
> on-disk state. Setup instructions: [install runbook](/install.md). Browsable
> version of this reference: [/api.html](/api.html).

## 1. CLI

One static binary, `flk`. Commands read and write flock state under
`FLOCK_HOME` (default `~/.flock`). Exception: `flk quota install-agent`
writes a LaunchAgent plist under `~/Library/LaunchAgents`, and the installed
job records the `FLOCK_HOME` in force when it was installed and runs against that root.

### Identity & configuration

| command | what it does |
|---|---|
| `flk init [--profile p] [--agent a] [--account n] [--owner o] [--email e] [--no-wire]` | one-shot onboarding: account + profile + default + wire. Prompts on a TTY. Idempotent. Does not perform the login (human, TTY) |
| `flk init --discover [--owner o] [--yes] [--only names] [--no-wire]` | adopt existing logged-in agent homes from four sources: the top level of `~` (`.claude`, `.claude-*`, `.codex`, `.codex-*`), the environment of running agent processes, a sweep under `~`, and profiles' own isolated homes. A home with no login is not proposed; a home already attached to a profile is skipped. Each proposed home is bound in one of four ways: a new account and profile (named from the identity's local part, deduped); a new profile on an account already on record whose declared address matches the login; attachment to an existing profile whose account declares the same address and whose current home holds no login; or re-attachment of a profile to its own isolated home, which clears the profile's external home pointer. `--only <names>` narrows adoption to the named candidates (names are the first column of the proposal). On confirmation each candidate is bound as listed and wired, and the conversation migration is applied; `--no-wire` skips both. Adoption does not copy tokens; `flk profile rm` detaches an adopted home and leaves its directory and login. Owner: `--owner`, else inherited when exactly one owner is on record across your accounts, else prompted on a TTY; a non-TTY run that would create an account with no owner to inherit errors. `--yes` skips the confirmation for non-interactive runs |
| `flk upgrade [--check]` | download the latest platform binary, verify its checksum, replace atomically. `--check` reports without installing |
| `flk account add <name> --provider <p> --owner <declared-owner> [--email <e>]` | register a named account. Provider and owner are required; the optional email enables the identity consistency check |
| `flk profile add <name> --agent claude\|codex [--account n] [--owner o] [--model m]` | create a profile: agent + account + isolated home. With `--account` omitted, an account named `<name>` is declared and printed |
| `flk profile login <name> [--agent a] [--owner o] [--email e]` | human-only TTY login into the profile's home; `--email` declares the address on the profile's account when the account has none. For claude, when `claude auth login --help` succeeds and no arguments follow `--`, the login is `claude auth login`, with `--email <address>` added when the account declares an address; otherwise Claude Code opens for `/login`. Checks the result (per `claude auth status`, else the state file) against the declared account (when the account declares an email and the home exposes a logged-in identity). Requires a TTY on both stdin and stdout and refuses when either is missing — an agent command bridge that captures output is not a terminal. With `--agent`, a missing profile is created first (account declared, home created, events wired). When no default profile exists, the created profile becomes the default |
| `flk profile ls` | list profiles with agent, account, and default marker |
| `flk profile rm <name> --yes` | delete the profile: its config, its isolated home directory (login state included), and everything wire installed. An external home attached via `set-home` is detached, not deleted, and the wiring inside it stays on disk: `flk uninstall` strips wiring from every profile home, including attached ones |
| `flk profile set-home <name> <path> [--force]` | point a profile at an existing external agent home (e.g. `~/.claude`). Both arguments are required; a path of `-` or `""` restores the isolated home. When the profile's account declares an address and the home's login (per `claude auth status`, else the state file) is a different address, the command refuses and names the profile whose account declares that address when one exists; `--force` binds it anyway |
| `flk profile wire` | install hooks, MCP server, commands, skill, and statusline into every profile home. Idempotent; writes are namespaced, except Codex's single-valued `notify` key, which is assigned and so replaces any existing value |
| `flk use <profile> [--pin]` | set the default profile, or pin via `.flock.toml` in the repo |
| `flk whoami` | machine-wide identity report. Flags a mismatch between a profile's declared account and its logged-in identity (when the account declares an email and the home exposes one) |
| `flk capabilities` | per profile: identity, model, MCP servers, wiring state |
| `flk welcome` | print each logged-in profile's capacity and the live session count |

Cobra's generated `help` and `completion` commands exist on every command and
are not documented per-row.

### Sessions & workers

| command | what it does |
|---|---|
| `flk new [--profile p] [--name n]` | launch an agent session under a profile, as a tmux window (name `<account>-main-<seq>` by default) |
| `flk ls` | roster with current state. Rendered states: `accepting` `working` `paused` `blocked(input)`. `paused` means the profile's worker capacity is determinately exhausted — the same test `claude:auto` uses to skip a worker. A session whose pane is gone is a record, not a state: it renders as `resumable` (transcript on file) or `stale record`. Hook statuses map onto these: `idle` and `started` render as `accepting`; `needs-attention` as `blocked(input)`. MODEL is the model and effort the agent last reported (claude through its statusline input, codex through its rollout's `turn_context`), `-` until it reports |
| `flk attach <name\|lead>` | jump to a session's pane. `lead` resolves to the current lead |
| `flk model <session> [<model>] [--effort e] [--default]` | the model lever. With no model, prints what the agent last reported. With a model and/or `--effort` (`low`, `medium`, `high`, `xhigh`), drives the agent's own `/model` picker from the composer at a turn boundary, reading each picker screen and the agent's confirmation, and fails closed (Escape) on any step it cannot read. Claude: `default`, `opus`, `fable`, `sonnet`, `haiku`, applied to the session only unless `--default` (the picker's Enter, which saves the pick for new sessions); a model without an effort selector (Haiku) takes the model and ignores the effort. Codex: a model slug such as `gpt-6-astra`; codex writes the pick to its `config.toml` as well, so it becomes the profile's default. Neither agent's `/model <name>` argument form is used: on claude it saves a default |
| `flk switch` / `flk ui` / `flk status [--tmux]` | picker popup / dashboard / one-line summary |
| `flk worker ensure <profile>` | spawn or reuse the account's worker (`<account>-worker-<seq>`; the sequence is the lowest number no live session holds, so a replacement reuses its predecessor's name). A worker gets its own tmux session. At most one live worker consumes an account; profiles sharing the account share it. Dispatch spawns no worker; a live worker-role session under the profile is chosen like any other live session of the profile (see `flk delegate`). This command is the manual way to hold a warm worker seat. A claude profile with no spawnable credentials is refused before the session is created (a session spawned there would boot to a login screen); complete `flk profile login` first |
| `flk adopt <tmux-target> [--name n] [--home dir] [--profile p]` | register a pre-flock session into the roster. Identity is read from the agent's own state; status is liveness-only (no hooks) |
| `flk restart <session>` / `flk restart --all` | restart live Claude conversations in place only after verified idle or hookless quiescence. The conversation id must be known, the pane must be live, and the composer must be positively recognized as empty (an agent's dim placeholder or prompt suggestion counts as empty). The command enters `/exit`, verifies the composer changed, exits gracefully, and resumes the same conversation in the preserved pane. Recognition and graceful-exit failures fail closed |
| `flk resume <profile> <conversation-id> [--name n] [--dir d]` | start a recorded conversation as a managed tmux session under the profile: the agent's first-boot prompts for the directory are pre-answered, the session is registered on the roster, named `--name` or `<account>-<first 8 characters of the conversation id>`, in `--dir` or the current directory. Refused when the profile's home holds no transcript for the id (claude: `projects/*/<id>.jsonl`; codex: `sessions/**/rollout-*-<id>.jsonl`), when the home holds no spawnable login (claude credentials; codex `auth.json`), when a tmux session of that name exists, or when the conversation is already live in a roster session |
| `flk migrate [--dry-run] [--allow-signal]` | converge attested pre-Flock Claude processes into resumed managed sessions. `--dry-run` prints the plan without stopping or spawning and is the required first pass before a standalone `flk migrate` in the install runbook (discovery applies migration under its own consent). The current process is skipped for last restart. Tmux candidates require verified idle or quiescence and fail-closed empty-composer recognition before graceful exit. Plain-terminal candidates are skipped by default; `--allow-signal` additionally requires process-tree quiescence, 120 seconds of transcript quiescence, and no non-shell children before SIGTERM. A resume failure after stop records `migrate-stranded` with the old pid, exact resume command, and error |
| `flk doctor [--prune] [--deep]` | environment checks: tmux and agent versions, structural hook and MCP wiring, credential and identity shape, task-ledger consistency, and drift-prone on-disk contracts. `--deep` probes the live usage endpoint and credential shape, requires network, and macOS GUI context only when Claude's token comes from the Keychain rather than a credential file, and prints the response shape if it changed. `--prune` clears vanished roster records, keeping task-referenced ones unless the record's pinned process is proven dead |

Human-driven claude session names converge on `<account>-<slug>`, with
collision suffixes when necessary. A session `flk new` launches is minted
`<account>-main-<seq>`; a claude session is renamed when its first transcript
slug appears (a codex session keeps the minted name), and migration can name a
resumed conversation from its known slug immediately. Delegation runs in the
target profile's live session (its member): the session named after the
profile when several are live, else the most recently updated; when none is
live, a session is spawned under the profile's name and keeps that name. The
dispatching session is never its own target; a dispatch to the caller's own
profile with no other live session is refused. Leadership does not rename a
session.

### Delegation & memory

| command | what it does |
|---|---|
| `flk delegate --to <profile\|<agent>:auto> "brief" [--wait] [--cross-owner] [--timeout d] [--isolation worktree] [--thread name] [--model m] [--effort e] [--keep-model]` | run a brief in the target profile's live session (its member): the session named after the profile when several are live, else the most recently updated; a live worker-role session under the profile is a candidate like any other. When none is live, a session is spawned under the profile's name (a claude profile with no spawnable credentials is refused first); when the dispatching session is the profile's only live session, the dispatch is refused. For a target without the worker role, the brief is delivered at a turn boundary: the target's roster status is `idle` or `started`, no codex turn is running in its rollout, and its composer is empty (the agent's dim placeholder or prompt-suggestion ink counts as empty); a draft in such a target's composer is never cleared. If no boundary comes within half the timeout, a member that was working the whole wait is PARKED: the task stays accepted with its brief on the ledger and is re-dispatched at a later turn boundary, with no delivery deadline promised. On a FIRST dispatch a draft or an unrecognized composer fails with that reason; a task that arrived already parked parks again instead. A codex dialog replaces its composer: the rate-limit reminder ("Approaching rate limits") is declined with Escape, which changes nothing, and recorded as a `codex-modal-dismissed` event; any other dialog fails the dispatch naming it, since it needs a human answer. A worker-role target is pasted after readiness, with composer residue cleared once. A target whose directory differs from the task's is told the task directory in the brief. `--wait` defaults to true; with `--wait=false` the dispatch runs in a detached child process that lives until the result is collected. `--timeout` bounds how long the dispatch waits for the target session's turn to finish (default 15m); it is recorded on the task, so a detached dispatch honors it. A dispatch whose sender and target accounts have different owners is refused unless `--cross-owner` confirms it; the confirmation is recorded on the task, and the gate runs only when the sender profile and both account owners are on record. `claude:auto` picks the least-consumed logged-in profile that is not determinately exhausted; `codex:auto` picks the least-consumed logged-in codex profile on a fresh rate-limit reading from its rollout (the numbers codex's `/status` shows), skipping a determinately exhausted one; with no reading, any logged-in codex profile. The chosen profile and the routing reason are printed. Only a one-line wake-up is pasted, carrying the task id and sender and naming where the brief is; the framed brief (task id, sender, target session name, result contract, clarification history, task directory) reaches a claude session inside that turn's context through its `UserPromptSubmit` hook, and a codex session through the `flock_task_brief` tool or `flk task brief <id>` named in the line. After the paste, dispatch confirms the turn started (the header in the codex rollout, or a lifecycle hook event for hook-wired agents), re-sending Enter while unconfirmed and failing at 45 seconds; a codex session with no rollout on record yet is not confirmed, and the dispatch proceeds. On completion, the turn's recorded input must contain this task's header, and exactly one header; otherwise the task stays `running` with an error naming the mismatch, settles if a later turn carries the header, and fails when twice the timeout passes without one. `--isolation worktree` creates branch `flock/<task-id>` from HEAD with the base commit recorded and sites the worktree at `<profile-workspace>/.flock/worktrees/<task-id>`; with no profile workspace the current directory stands in, and when that path lies inside the repository the worktree goes to `~/.flock/state/worktrees/<hash>/<task-id>`. The dispatch refuses if that path falls outside the workspace root of a session with the worker role; a session without the worker role is not sandboxed to a root. The brief carries the write scope, and merging plus worktree removal remain the sender's steps. `--thread` tags the task into a named thread. `--model` and `--effort` borrow a model and reasoning effort for the task: at the boundary the target is switched through its own picker (see `flk model`), the wake-up follows, and after the turn a restore of the previous model and effort is ATTEMPTED whatever the outcome, unless `--keep-model`; a restore that does not happen is recorded in `model_note`; the task records `model`, `effort`, `prev_model`, `prev_effort`, `model_switched`, `model_restored`, and a `model_note` when a restore did not happen. A detached dispatch is restored when reconciliation settles it. Brief lint is advisory: a brief under 200 characters or without a deliverable warns and never refuses |
| `flk task ls\|status <id>\|result <id>\|brief <id>` | the delegation ledger; `brief <id>` prints the framed brief a task's target session must receive, as the wake-up line points at it. A worker that hits blocking ambiguity ends its turn with a message beginning `CLARIFY:` (stating the exact ambiguity, why it decides the deliverable, the smallest question set, safe work done, and its declined-to-decide default), and the task settles as `needs-clarification` with the questions as its result |
| `flk msg send <session\|lead> "text"` / `flk msg ls` / `flk msg read` | turn-boundary fleet messages. A send that resolves its target and writes the mailbox succeeds regardless of whether the reader is busy: the message lands in the target session's mailbox, a typed `message` event joins the events stream, and delivery is attempted only at a turn boundary: a worker gets a one-line notice pasted (queued input if it is mid-turn), A human-driven session with a live pane can receive a typed wake-up line at a verified turn boundary. Where hook delivery is available, the waker first allows three minutes for it to deliver the notice; the wake attempt gives up after 45 minutes. Paneless sessions rely on their configured notification surface. The line is submitted in that session's composer as its next turn; if the read-back finds other input beside it, it is left in the composer unsent, and a composer that already holds a draft refuses it. `lead` resolves through the lease at send time; with no valid lease the message waits in a pending-lead box the next holder claims on read. Use messages for coordination; task briefs go on the delegation ledger. `doctor --prune` clears read messages older than seven days; unread mail is never pruned |
| `flk task clarify <id> "answers"` | answer a `needs-clarification` task and redispatch it: the round is appended to the one task record (question, answer, timestamps, and a worktree snapshot when isolated), the full brief plus history replays to the same worker as the canonical authority, and the timeout restarts. Three rounds per task; past that, re-scope the brief |
| `flk task review <id> --to <profile> ["focus"]` | builder-to-reviewer on the ledger: dispatches a review of a completed `--isolation worktree` task, with the handoff package assembled from artifacts — the original brief, branch, base and HEAD pinned as commit SHAs, `git diff --stat`, and the builder's result verbatim. The reviewer works read-only in the builder's worktree (contractual; the worktree HEAD is checked after review and a move warns) and ends with one line, `REVIEW: approve\|revise — <sentence>`, settled typed on the review task (`approve`, `revise`, or `unknown` when malformed — completion never fails on it). The review joins the builder's thread, minting `t-<task-id>` when it has none. Merge stays the sender's step, now with a verdict beside it |
| `flk thread <name>` | the thread's tasks in order — id, state, reviewer verdicts, first line of brief and result: the durable cross-task narrative that survives session recycles |
| `flk brief [--note "text"]` | show or append the project brief `.flock/brief.md`. Notes carry author and timestamp |
| `flk recall <query>` | search all profiles' transcripts; returns matches with resume commands |

### Lead protocol

**The lead delegates; it does not do the work.** The lead's role is limited
to sharding, dispatch, verification of returned results, and reporting. The
dispatch frame states the worker's role; the lead's is the mirror image. Do
not drive worker panes with raw tmux keystrokes: every first-boot screen,
login, and composer snag has an `flk` command (or is a bug to file), and
keystrokes bypass the ledger. If you find yourself editing code in the lead
session, stop and cut a brief instead.

The lead assignment is stored as a lease file, `state/lead.json`, recording
holder, pane, pid, and the grant chain (rendered publicly as the lead
history). "Lease" in the rest of this reference names that file.

| command | what it does |
|---|---|
| `flk lead grant <session>` | designate the lead. Owner-run by policy; the CLI does not authenticate its caller. Claude and Codex sessions are eligible, subject to admission checks; Gemini sessions are refused. The lease records pane and pid and is verified against the live process. Claude leads are expected to run Fable; a Claude lead on another model gets a statusline warning |
| `flk lead show` | current lease, holder liveness, grant chain |
| `flk lead recuse --successor <session\|<agent>:auto> [--reason r]` | recusal; the caller must hold the lease. Claude and Codex successors are eligible, subject to admission checks. `<agent>:auto` requires a live candidate with lead capacity, and returns an error otherwise |
| `flk lead check` | prints a retirement order when the lead's reading is determinate and any quota window is at or above 98%. On an indeterminate reading (`unknown`, `stale`, `drift`) it prints the reason and no order |
| `flk lead enforce` | moves the lease to an already-live worker with lead capacity, when the current lead's capacity reading is determinate and exhausted; it also upgrades a legacy pin and lets the same identity retake its own lease. An indeterminate reading (stale, unknown, drift) causes no action; the reason is printed. Run automatically by each quota fetch tick |

### Quota

| command | what it does |
|---|---|
| `flk usage [--live]` | consumption computed from transcripts; `--live` fetches authoritative quota instead |
| `flk quota fetch [--quiet]` | authoritative per-profile quota: claude via each profile's own token (requires macOS GUI context for the Keychain); codex from its rollout, where every turn records the 5-hour and weekly windows codex's `/status` shows, read from the live session's thread or the newest rollout under the home. Writes the cache, records capacity-transition events, and runs lead-capacity enforcement in the same tick. A codex entry is also refreshed at each codex turn by the notify hook, and its age is the turn's age, not the fetch's |
| `flk quota show [--json]` | render the cached quota anywhere, including over SSH |
| `flk quota install-agent` / `uninstall-agent` | macOS LaunchAgent, 15-minute interval. Each tick refreshes quota and runs lead-capacity enforcement; installing it enables automatic succession. `uninstall-agent` removes it |

Capacity is tiered. Worker (dispatch) capacity: a profile is exhausted when
its 5-hour or weekly-all window is at or above 98%. Dispatch refuses it,
`claude:auto` skips it, and the statusline shows ⛔ with the reset time. A
model-scoped window at 98% does not block dispatch. Lead capacity: any window
at or above 98%, model-scoped included, disqualifies a lead. A codex profile
has the same 5-hour and weekly windows, read from its rollout's `token_count`
rate limits (the numbers codex's `/status` shows) at every codex turn and at
fetch time; the reading's age is the turn's age.

A capacity reading is trusted only when determinate: fresh, and in a
recognized response shape. Otherwise it carries a reason (`unknown`, `stale`,
or `drift`), and two rules apply: dispatch refuses only a determinately
exhausted profile, so an indeterminate profile remains routable; succession
and retirement take no action on an indeterminate reading, and the statusline
shows `⚠ quota <reason>`.

## 2. MCP tools

`flk mcp` serves MCP over stdio; `flk profile wire` registers it as server
`flock` in each profile home. Claude registration is written to the profile's
Claude state file; Codex registration is written under `mcp_servers.flock` in
`config.toml`. Native tools load at session start. An agent must inspect the
tools exposed to its own session before promising to call one; another profile's
wiring does not prove this session has loaded the server.

| tool | args (\* = required) | returns |
|---|---|---|
| `flock_roster` | (none) | `{live: [...], dead: [...], lead: {...}}` |
| `flock_delegate` | `to_profile`\*, `brief`\*, `mode` (`wait` default \| `async`), `cross_owner` (`'true'` confirms a cross-owner dispatch), `isolation` (`worktree`), `thread`, `model`, `effort`, `keep_model` (borrow a model and effort for the task; restored after the turn unless `'true'`) | wait: `{task_id, routing, state, result, error}` · async: `{task_id, routing, state, note}`. Async runs inside the MCP server process, which must stay alive until collection; unlike CLI `--wait=false`, it does not detach |
| `flock_msg_send` | `to`\*, `text`\* | mailbox write + turn-boundary delivery attempt |
| `flock_msg_ls` | (none) | this session's mailbox, unread first |
| `flock_msg_read` | (none) | unread messages, marked read on return |
| `flock_task_review` | `task_id`\*, `to`\*, `focus` | dispatches the review; returns `{task_id, thread, state, verdict, note, result}` |
| `flock_thread` | `name`\* | the thread's tasks in order, briefs and results trimmed to first lines |
| `flock_task_clarify` | `task_id`\*, `answers`\*, `mode` | answers a needs-clarification task and redispatches it (same record, same worker, three-round cap) |
| `flock_task_status` | `task_id`\* | task record, result omitted |
| `flock_task_brief` | `task_id`\* | the framed brief the task's target session must receive (what the wake-up line points at); records the delivery |
| `flock_model` | `session`\*, `model`, `effort`, `default` | with only `session`: the model and effort the agent last reported. With `model` and/or `effort`: the model lever (see `flk model`); returns the previous model and effort |
| `flock_task_result` | `task_id`\* | full task record |
| `flock_handoff` | `to_profile`\*, `context`\* | `{session, brief, hint}`; appends a HANDOFF note and spawns a session pointed at it |
| `flock_note` | `text`\* | appends to `.flock/brief.md` with author and timestamp |
| `flock_recall` | `query`\* | matches with resume commands |
| `flock_lead` | (none) | `{lead, session_id, profile, granted_by, granted_at, alive, chain_len}` |
| `flock_lead_recuse` | `successor`\*, `reason` | the new assignment; the caller must be the current lead |
| `flock_usage` | `session`, `live` (bool) | transcript stats, or authoritative quota per profile with per-profile cache fallback |
| `flock_capabilities` | (none) | per-profile capability map |

Rules for agents using these tools:

- Treat account virtualization as one interface to isolated accounts, each with
  its own login and limits. Delegate routes to an eligible account, using fresh
  quota data for Claude when available. Automatic routing ranks candidates by the
  lowest worst-window percentage when every candidate has a fresh reading, and by
  recorded transcript usage otherwise.
  The task ledger names the target profile and the routing reason, and recall searches transcripts
  across accounts when invoked. Lead, worker, profile, and lease names are
  implementation details unless they explain routing or a refusal.
- The worker cannot see your session. A brief must carry the goal, all
  needed context, the deliverable format, and acceptance criteria. Include
  executable file paths and commands in the brief.
- `claude:auto` reports the account chosen; repeat that to the user.
- Dispatch to a determinately exhausted profile is refused; do not work
  around the refusal. If any of your own windows reaches 98% while you hold
  the lead, recuse (see the runbook's capacity section). A worker whose
  only window at 98% is model-scoped may still accept work on other models.
- On task failure, report the state, error, and log path verbatim.
- Preserve a `flock_msg_send` deaf-mail warning. The mailbox write succeeds,
  but a target without a hook or statusline surface can wait silently; do not
  describe that warning as delivered coordination.

## 3. Hook & event contract

`flk profile wire` installs these. They call `flk event …` (hidden command;
errors go to stderr with exit 0, so a hook failure never fails the agent).

Claude Code: every hook posts its stdin JSON to `flk event claude`:

| hook | roster status |
|---|---|
| `SessionStart` | `started` |
| `UserPromptSubmit`, `PreToolUse` | `working` (throttled to 3s) |
| `Notification` | `needs-attention` |
| `Stop` | `idle` |
| `SessionEnd` | `ended` |

Payload fields used: `hook_event_name`, `session_id`, `transcript_path`,
`cwd`, `tool_name`, `message`, `prompt`. Session resolution order: tmux pane →
`FLOCK_ID` env → `session_id`. Unknown sessions register as `external` with
deterministic id `ext-<session_id[:8]>`: one record per agent session. On
`UserPromptSubmit`, `prompt` is recorded as the turn's input for dispatch
integrity, and when it carries a delegation wake-up line the hook prints that
task's framed brief to stdout, which Claude Code adds to the turn's context:
this is how a delegated brief reaches a claude session.

The first Claude `SessionStart` or `UserPromptSubmit` event consumes a durable
first-contact flag and writes one context notice into Claude's conversation. It
names the roster identity and profile, states whether lifecycle surfaces are
wired, and, when wired, says that native tools load after restart. It tells the agent to operate
Flock on the user's behalf without exposing commands. Silent hook events never
consume the flag. The statusline input (`model.id`, `model.display_name`) is
recorded on the session whenever it changes; it is what `flk ls` and the model
lever read for a claude session. Codex receives its context through managed `AGENTS.md` and MCP,
not through a hook context channel; a delegated brief reaches a codex session
through `flock_task_brief` or `flk task brief <id>`, named in the wake-up line.

Codex: `notify` in the flock-owned `config.toml` calls
`flk event codex-notify <json>`. Only `agent-turn-complete` is ingested; it
sets `idle` and records the turn's final message and input for result
collection, and the model and effort of the turn from the rollout's
`turn_context`. Codex has no start or working hooks: boot is discovered from the
profile home's session files, and death from pane liveness. Flock creates no
profile home at `~/.codex`, but discovery can adopt that directory, and wiring
then writes its `config.toml` like any other profile home.

## 4. State on disk

Flock state lives under `FLOCK_HOME` (default `~/.flock`), as plain files.
Two long-lived processes exist. Each wired agent session runs `flk mcp` as a
stdio child for as long as that session lives. On macOS, the optional quota
LaunchAgent runs `flk quota watch`, restarted on unsuccessful exit, with its
refresh schedule set by the watcher itself (next reset deadline or a requested
refresh) rather than by the plist interval alone. `flk quota uninstall-agent`
removes the LaunchAgent. Outside
Flock's state directory: the binary itself, external profile homes
set with `set-home`, and the macOS LaunchAgent plist.

```
~/.flock/
  accounts.toml                 # named accounts: provider, owner, email
  profiles/<name>/
    profile.toml                # agent, account, model, workspace, env
    home/                       # the agent's isolated home (CLAUDE_CONFIG_DIR / CODEX_HOME)
  state/
    sessions/<id>.json          # roster records (atomic writes)
    events.jsonl                # append-only log: lifecycle, capacity-transition, succession, message
    lead.json                   # the lease: holder, pane, pid, granted_by, chain
    quota.json                  # quota cache (per profile, timestamped)
    tasks/                      # delegation ledger + per-task logs
    messages/                   # mailboxes, one box per session + lead-pending
```

Environment set by flock in spawned sessions: `FLOCK_ID` (roster id),
`FLOCK_PROFILE`, and the agent's home variable (`CLAUDE_CONFIG_DIR` or
`CODEX_HOME`). `TMUX_PANE` comes from tmux.

## 5. Invariants

- Quota reads create no separate token store: they use each profile's own
  token in place (Keychain or the agent's credential file) and copy it nowhere.
  That is narrower than "no secrets stored", and the difference matters. A
  profile's ISOLATED home lives beneath the Flock root and holds that account's
  own login files, written there by the provider's own tools. A task's brief
  and its extracted result are stored on the ledger as given, with no secret
  redaction, so anything you put in a brief is on disk in plain JSON.
- No network listeners. Flock's MCP server is stdio; nothing binds a port.
- Namespaced writes, with named exceptions. Hook entries and user MCP entries
  are marked and idempotent and are left alone. Three single-valued settings are
  ASSIGNED rather than merged, so an existing value is replaced: Codex's
  `notify`; the Claude `statusLine`, which is preserved only when its command
  does NOT contain "statusline", so a custom statusline named for what it is
  gets overwritten; and, in an isolated Codex home, the Flock MCP entry's
  command, args and `default_tools_approval_mode`. `flk profile rm` deletes the whole profile: config, isolated
  home directory with its login state, and everything wire installed. An
  external home attached via `set-home` is detached, not deleted.
- The lease records pane and pid and is verified against the living process.
  Identity is checked for consistency against the provider's own record when the account
  declares an email and the agent home exposes a logged-in identity. A
  designation claim is checked against the lease before it is accepted.
