Codey publishes its lifecycle — a session opened, a run started, a tool ran, an agent stopped to ask you something — so that things outside the process can watch it. That is a different job from the event stream the TUI draws from: this one carries no conversation content, no text deltas and no tool output, because its audience is a sidebar, a notifier, or a script, and what the model wrote is not theirs to have.
| Event | When |
|---|---|
session_start / session_end | one conversation, from open to close; a second tab is a second session |
run_start / run_end | one prompt, from your message to the model's final answer, tool calls and all |
turn_start | one model call inside a run |
tool_start / tool_result | one tool call; the result says whether it failed, not what it returned |
blocked / unblocked | waiting on a human: an approval prompt, or a question from ask_user |
paused / resumed | the loop held at a turn boundary, and let go again |
blocked is the one you cannot get any other way. From outside the process an
agent thinking hard and an agent that has been sitting on an approval prompt
for ten minutes look exactly alike: the run has not returned and nothing is
being printed. Only one of them will finish without you.
Hooks in the config run shell commands:
{
"hooks": {
"run_end": ["notify-send 'codey' \"$CODEY_SESSION finished\""],
"blocked": ["notify-send -u critical 'codey needs you' \"$CODEY_MESSAGE\""]
}
}
Each command gets the event twice over: as CODEY_HOOK_EVENT, CODEY_SESSION,
CODEY_AGENT, CODEY_TURN, CODEY_TOOL, CODEY_REASON, CODEY_MESSAGE and
CODEY_ERROR — always exported, sometimes empty, so a one-liner never has to
test first — and as the same fields in JSON on stdin, for anything that wants a
parser rather than string surgery. They run in the working root, one after
another, on the agent's own goroutine, which is why each is given ten seconds
and then abandoned: a hook that hangs would otherwise be a session that hangs.
Output is swallowed unless the command fails, since the TUI owns the screen; a
failure is written to the event log as a hook_error and changes nothing about
the run. An unknown event name is refused at startup rather than ignored,
because a hook that silently never fires is the one bug with nothing to notice.
This is the single setting the wizard never writes. A hook is a command your
machine runs on your behalf, so it gets into the file because you typed it
there. -no-hooks turns the lot off for one run.
herdr is a terminal workspace manager for coding agents: a background server owns the terminals, panes survive a detach, and a sidebar shows across every project which agent is working, which is blocked, and which is idle. Codey reports itself to herdr, automatically — there is nothing to install and nothing to configure. Run it in a pane and it says so at startup:
codey: herdr detected — reporting agent state to pane w1:p1
Herdr reads most agents' states off the screen with pattern rules. Codey is
told instead, through the contract herdr documents for agents it does not ship
support for: a process inside a pane inherits HERDR_ENV=1, HERDR_PANE_ID
and HERDR_BIN_PATH, and reports semantic state by running herdr pane report-agent. Being told beats guessing in both directions — herdr never has
to infer anything from what the TUI happened to draw, and the state it could
not see at all, that the agent is sitting on an approval prompt behind a
full-screen view, is stated outright along with what it is waiting for:
bash: rm -rf build
asked about Storage, Error handling
A subagent's prompt counts. Spawning does not block, so a background agent can ask for approval long after the agent that spawned it went quiet, and a pane that called that idle would leave you waiting on an agent that is waiting on you. Two agents can be waiting at once, and answering one does not clear the other: the pane goes back to what it was doing only when the last prompt is answered.
One pane holds one Codey and one Codey can hold several tabs, so the tabs are reduced to the single state the pane is in: blocked beats working beats idle. What a person scanning a sidebar needs to find is the agent that stopped and wants them, and after that the one still going. A paused agent reports idle rather than blocked — nobody is being waited on, and it has no business in the list of agents that need attention.
Every report carries Codey's session id, so herdr's pane and agent APIs show
which conversation is running in a pane and a restored one can be pointed back
at it with -resume. Herdr restarts an agent by itself only where it knows how
to launch it, which for now means the agents in its own registry; the reference
is reported regardless, because it costs one flag and is the half of the
problem that is ours.
Reporting is a no-op anywhere else. Detection is those three environment
variables and nothing else — no probing, no socket, no cost to anyone who has
never heard of herdr — and all three are required, because a stale HERDR_ENV
exported into a shell that outlived its pane is exactly the case where
reporting would go nowhere, slowly, forever. -no-hooks disables it too.
When a state looks wrong, herdr agent list shows what herdr sees, and the
hook_error lines in .codey/events.jsonl show what Codey could not tell it.
Reporting is one direction. The other is the agent driving herdr itself — splitting a pane, starting a server in it without stealing your focus, reading what another pane printed, waiting until another agent is genuinely blocked rather than merely slow. Herdr publishes a skill that teaches an agent to do that, and the setup wizard offers to install it when it finds herdr on the machine:
Install herdr's skill, so the agent can drive herdr?
▌ ◆ yes, install it ~/.agents/skills/herdr
◇ no
Yes downloads herdr's SKILL.md
and writes it to ~/.agents/skills/herdr/SKILL.md, which is where Codey looks
for personal skills — the same layout herdr publishes and the same one
npx skills add writes, so a skill installed by either is found by both. From
the next start it is listed to the agent like any other skill, and invoked the
same way.
It is fetched rather than shipped inside the binary. The file is a manual for a
CLI that is still moving, and a copy frozen into a Codey release would go on
teaching last year's flags long after they stopped working. The cost is that
the install needs the network: when it fails, nothing is written, the wizard
says why, and the answers you just gave are saved regardless — only the
download is lost, and /setup offers it again.
Nothing is written unread. What comes back is parsed by the same loader that will read it at startup, and rejected if it is not a skill Codey can load — a login page, an error page, or a file whose shape has changed. The write goes through a temporary file in the same directory, so a failed update leaves the copy you already had rather than half of a new one.
This is the only question in the wizard that acts rather than records, so it is
the only one where pressing enter through the screens does something: the row
names the exact path before you answer, and the notice afterwards names the
file that was written. Uninstalling is rm -r ~/.agents/skills/herdr.