Agents: Claude Code and Codex¶
bosun runs coding agents in tmux windows and needs to know a few things about each: how to start and resume it, whether it is busy, whether it can take a message, what it is allowed to do. An agent is called a harness in the code. There are two today.
| Claude Code | Codex | |
|---|---|---|
| Repo sessions | Yes | Yes |
| The orchestrator | Yes | Not yet: it stands on Claude Code's hooks, plugins and permission settings |
| Chosen by | The default | BOSUN_HARNESS=codex, or harness: codex on a window in sessions.yml |
| Unattended flags | BOSUN_CLAUDE_FLAGS: --permission-mode auto --chrome |
BOSUN_CODEX_FLAGS: --no-daemon --approve-for-me |
| Model | BOSUN_MODEL, or the window's model: |
BOSUN_CODEX_MODEL (empty: Codex's own default), or the window's model: |
| Its files, read through | lib/claude_state.py |
lib/codex_state.py |
A setup can mix them window by window.
How it is built¶
Shared code never asks which agent it has. Each layer has one place per agent, and one table or flow over them:
| Layer | Shared | Per agent |
|---|---|---|
| Checking a machine | lib/preflight.sh, the flow |
lib/harnesses/<harness>.sh, a profile |
| Starting a window | libexec/bosun-launch, the flow: resolve the session, refuse a second copy, start or resume |
lib/launch/<harness>.sh: build_agent_args, first_prompt, run_new, run_resumed |
| A running session | lib/agent_state.py: the Agent interface and the AGENTS table. lib/agents.sh is its shell side |
lib/claude_state.py, lib/codex_state.py, each a module with Agent's functions (mypy checks they match) |
| Reading a screen | lib/pane_input.py: typed_text |
A reader per agent in BOX_READERS |
window_specs is the one source of which agent a window runs: window_harness and harnesses_in_use read it.
A running agent's record has the fields of Claude Code's running-session record, plus harness, so what goes
by a window's status (send, sessions, check-ins, the watchdog) works for both without knowing which it
has. Adapters share lib/agent_files.py, and neither imports the other.
What only one agent has stays out of the interface and is called by name: Claude Code's login checks
(lib/login.py), its bypass warning, and the transcripts Impact and learning read.
What Codex needed¶
All of this was found by running real Codex 0.166.0 on a real Mac, and none of it is a public API.
--no-daemon. By default the terminal attaches to a shared background daemon, which holds the session
file. With this flag each process holds its own, which is how bosun tells which session runs in a window.
--approve-for-me, not a bare sandbox. Under --sandbox workspace-write a session cannot read the
Keychain, so a stored GitHub login is out of reach. This flag keeps the sandbox and adds an automatic
reviewer that decides when a command may go past it, with no prompt. It is the closest thing to Claude Code's
auto mode. On top of that, lib/launch/codex.sh adds what a task always needs:
- the repo's
.gitas a writable root, since Codex keeps it read-only and nothing could be committed - bosun's state and log folders as writable roots, for
bosun ledger report - network access, for
gitandgh
A first prompt, always. Codex writes no session file until its first prompt. A Codex with no file looks
just like one stopped at a screen, so bosun launch gives a new session a short prompt if it has nothing
to do yet. That costs one small request, once per session.
Trust lives in config.toml. [projects."<dir>"] with trust_level = "trusted", which bosun trust
writes. Passing it with -c does not skip the screen. On that screen Enter accepts, unlike Claude Code's,
where Enter exits, so send must never reach it (below).
Names are bosun's. Codex has no way to name a session from outside, so bosun keeps a table in
codex-sessions.json, filled in by what Codex runs at the end of a turn (notify=, bosun hook
codex-turn-end). That report has two traps, both found only by running the real thing:
- It also fires for Codex's hidden helpers, such as the one that writes a session's title, each under an id of its own. A thread only counts if it has a session file that came from the terminal.
input-messageslists every prompt of the session so far, on every turn. So bosun counts what it has seen, per session, or old messages would come back as things you just said.
A turn is task_started to task_complete or turn_aborted in the session file. The reviewer has
session files of its own, marked as such.
Questions are on the screen only. A session asking for approval still reads as busy, because the file
does not record the question, and Enter would answer it. So bosun send reads the screen first and refuses
(lib/pane_input.py). With the default flags Codex does not ask, so this is a guard, not a usual path.
What Codex windows do not have yet¶
| Feature | On Codex |
|---|---|
| The orchestrator | Claude Code only, and it must be installed. BOSUN_ORCHESTRATOR="" runs windows with no orchestrator and stops preflight asking for it, but bosun init, install.sh and bosun doctor still require the claude command |
| Remote Control from your phone | No such thing: drive the window from a terminal |
| A revoked login | Not reported. codex login status only says a key is there, and a failed request shows on the screen but not in the session file |
| What you said in a window | Recorded when the turn ends, not when you press Return, because that is when Codex reports it |
The watchdog's waiting, spend-limit and context-size |
waiting never fires, whatever the flags: the session file records only idle or busy, so a question reads as busy. Only bosun send reads a question, from the screen (above). The other two read Claude Code's transcripts |
| Impact stats and daily learning | Read Claude Code's transcripts, so Codex sessions are not counted. lib/learn_digest.py's Harness protocol is where an adapter goes |
| Autocompact | Claude Code only: BOSUN_AUTOCOMPACT_TOKENS and autocompact: do nothing on a Codex window |
shell, idle-bg and the watchdog's long-shell |
Codex's session file records only idle or busy, so a background command left running does not show |
| Per-task model and effort on a running session | A task's settings reach a running Claude Code session through a subagent, an instruction that names its Agent tool. A running Codex session gets the task at its own settings. A window opened for the task (--new) does start at them |
bosun resolve |
Claude Code's names only. A Codex window's are in codex-sessions.json |
| Compact-context and signal hooks | Claude Code only. Codex has hooks, but each new one stops at a review screen until a person accepts it, which is why bosun uses notify= |
| A real GitHub login inside a session | Not tested: the runs so far used a stand-in gh. A real one is expected to go through the reviewer, since the sandbox blocks the Keychain |
Adding another¶
- A profile in
lib/harnesses/<harness>.sh, so preflight can check it (tests/unit/test_preflight.pychecks it has every function). - A
lib/<harness>_state.pywith the functions ofAgent, and a line inAGENTS(lib/agent_state.py).mypychecks it has them all. lib/launch/<harness>.sh, with the functions oflib/launch/codex.sh.- A stand-in in
tests/sandbox/, a section oftests/sandbox/fresh_user.sh, and a run with the real thing:tests/sandbox.sh mac live <harness>. The stand-in only behaves the way you already believed: every trap above was found by the real run, so teach the stand-in each one afterwards. - The docs: this note, Setup, the README's Status section,
Known limits and
ROADMAP.md.