Using little-loops with Codex CLI¶
Running orchestration CLIs¶
All orchestration tools (ll-auto, ll-parallel, ll-action, ll-harness, ll-loop, ll-sprint) support Codex as the backing host CLI. Set LL_HOST_CLI=codex to select it explicitly:
LL_HOST_CLI=codex ll-auto
LL_HOST_CLI=codex ll-parallel --workers 3
LL_HOST_CLI=codex ll-action manage-issue --json
LL_HOST_CLI=codex ll-loop run my-loop
LL_HOST_CLI=codex ll-sprint run v2-launch
Auto-detection¶
You do not need to set LL_HOST_CLI explicitly if codex is on PATH. resolve_host() in host_runner.py probes available binaries in order and selects CodexRunner when codex is found and no override is set.
Detection order (first match wins):
LL_HOST_CLIenvironment variableLL_HOOK_HOSTenvironment variable (falls back toclaude-codewhen absent)- Binary probe:
claude→codex→ error
You can also set the host permanently in .ll/ll-config.json (or .codex/ll-config.json):
Invoking skills¶
After running ll-adapt --host codex --apply (see Getting Started), all /ll:* slash commands are available in the Codex TUI:
/ll:manage-issue enhancement improve ENH-001
/ll:scan-codebase
/ll:prioritize-issues
/ll:create-sprint
Skills are installed to ~/.codex/skills/<name>/SKILL.md. Re-run ll-adapt --host codex --apply after upgrading little-loops to pick up new or updated skills. The same run also merges an [mcp_servers.ll-mcp] definition into Codex's global
~/.codex/config.toml (or $CODEX_HOME/config.toml), registering the ll-mcp server
(FEAT-3138, corrected under BUG-3178).
pre_tool_use (ENH-1718)¶
The PreToolUse hook is wired by default, scoped to the Edit|Write matcher
in .codex/hooks.json — it does not fire for Read/Grep/Bash or other
tool calls:
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash {{LL_PLUGIN_ROOT}}/hooks/adapters/codex/pre-tool-use.sh",
"timeout": 5,
"statusMessage": "Checking learning-test coverage..."
}
]
}
]
It dispatches Write/Edit calls to the FEAT-1742 learning-test
discoverability gate (a no-op unless learning_tests.enabled is set) and
adds ~10ms per matched call — well under the 200ms hot-path threshold. If
you need to disable it, remove the PreToolUse entry from
.codex/hooks.json; Codex will prompt you to re-trust the modified file
either way.
Rollout files (session logs)¶
Codex writes one rollout JSONL file per session at
~/.codex/sessions/YYYY/MM/DD/rollout-<ISO-timestamp>-<uuid>.jsonl, keyed by
date, not by project — there is no ~/.codex/projects/. Archived threads
(codex archive <session-id>) move flat into ~/.codex/archived_sessions/
(confirmed on codex-cli 0.152.1: no YYYY/MM/DD/ subdirectory there).
get_project_folder(host="codex") therefore always returns None. Use
little_loops.session_store.sessions.detect_sessions(cwd, "codex") instead
(FEAT-3417): it queries the newest ~/.codex/state_*.sqlite's threads
table (matching both the caller's cwd spelling and its resolved form),
falling back to a scan of sessions/ and archived_sessions/ when the DB is
absent, unusable, or lacks the expected columns. iter_events(handle)
dispatches to parse_codex_rollout, which yields every record's host-native
payload untouched — including record types not yet documented (Codex has
added world_state at the top level and reasoning/item_completed
subtypes since the 0.98.0/0.130.0 corpus this was first verified against;
the parser passes all of them through rather than enumerating a fixed
vocabulary) — except response_item/custom_tool_call (name ==
"exec") and its paired custom_tool_call_output, which
CodexNormalizer (ENH-3433) replaces with Claude-shaped assistant/user
records so ll-logs can see Codex shell activity: the model-authored JS
snippet's cmd: value becomes a Bash tool_use, and the output becomes a
tool_result whose is_error is True only when the paired
event_msg/item_completed item (item.type == "CommandExecution")
reports status == "failed" or a non-zero exit_code — the output text
itself carries no failure marker (the "Script completed\nWall time N
seconds\nOutput:\n" header is identical on success and failure). That
CommandExecution item still passes through untouched as its own
event_msg event; it carries command (["/bin/zsh", "-lc", "<cmd>"] on
0.152.1), cwd, status, exit_code, stdout, stderr,
aggregated_output, duration, and formatted_output. Codex has no
/ll: skill-dispatch or queue-operation analogue, so those two ll-signal
carriers are not applicable to Codex; kimi-code has no normalizer yet
(remaining gap).
Per-turn token usage is present directly in the rollout file
(event_msg.payload.type == "token_count") — see
HOST_COMPATIBILITY.md [^tok-codex]
for the complementary codex exec --json stdout source. Captured real-shape
fixtures live at scripts/tests/fixtures/codex/ with a re-capture rule keyed
to cli_version.
Current Limitations¶
--agent (persona selection)¶
CodexRunner has no native --agent CLI flag (this is the permanent native gap tracked in ENH-1531 / openai/codex#10067). Instead, ENH-1533 implements a prompt-injection workaround: when CodexRunner.build_streaming(agent=…) is called, it reads .codex/agents/<name>.toml, extracts developer_instructions, and prepends a [Persona: <name>]\n<instructions>\n\n---\n\n block to the prompt payload. No warning fires when injection succeeds, and describe_capabilities() reports agent_select.status == "partial".
Setup: Run ll-adapt --host codex --apply once to generate .codex/agents/*.toml files from ll's agents/*.md definitions. Re-run after adding new agents.
Fallback path: When the TOML file (or its developer_instructions key) is absent, CodexRunner emits CapabilityNotSupported and prints a [ll] Warning stderr notice that names the dropped persona and points at ll-adapt --host codex --apply. The session proceeds with Codex's default model configuration.
Native-flag gap (permanent). Research (ENH-1531, thoughts/research/codex-agent-selection.md) confirmed no Codex CLI mechanism selects a named profile at invocation time:
- The codex CLI has no --agent flag. Feature request: openai/codex#10067 (open, no timeline).
- CODEX_AGENT and CODEX_PROFILE environment variables do not exist.
- The agents.<name>.config_file config stanza only governs spawn_agent subagent calls within an existing session, not the root session persona.
The injection workaround is therefore the only way to get ll-defined persona behavior under Codex; interactive Codex TUI sessions can additionally select agents via --agent <name> once the TOML files exist.
Note for CI/ll-doctor consumers: Before ENH-1533, ll-doctor exited 1 on Codex hosts because agent_select was "unsupported". With prompt injection, agent_select is "partial", which does not trigger exit 1. Codex hosts that previously failed ll-doctor solely on agent_select will now exit 0 — but ll-doctor's exit code is no longer tied solely to host capabilities: it also folds in the default install-surface checks (Entry Points, Skills & Commands, Decisions Store, History DB, FSM Loop Validity) and, under --full, the ll-verify-* / ll-check-links family, any of which can independently fail exit 0 at error-tier severity (FEAT-2793/FEAT-2795).
--tools (tool allowlist / sandbox modes)¶
Codex does not support a fine-grained tool allowlist (--tools parameter emits
CapabilityNotSupported and is dropped). Instead, use the sandbox_mode=
parameter on CodexRunner build methods (ENH-1529) to constrain execution:
sandbox_mode value |
Codex flag |
|---|---|
None (default) / "off" |
--dangerously-bypass-approvals-and-sandbox |
"read-only" |
--sandbox read-only |
"workspace-write" |
--sandbox workspace-write |
"danger-full-access" |
--sandbox danger-full-access |
Invalid sandbox_mode values raise ValueError. All three build methods
(build_streaming, build_blocking_json, build_detached) accept the
parameter.
json_schema inline dict (tool schemas)¶
CodexRunner.build_blocking_json supports json_schema via a file-mediated bridge (ENH-1530): the schema dict is serialized to a temp file and --output-schema <path> is appended to the Codex invocation args. The temp file path is returned in HostInvocation.cleanup_paths; callers must unlink it after the subprocess completes:
inv = runner.build_blocking_json(prompt=..., json_schema=my_schema)
result = subprocess.run([inv.binary, *inv.args], ...)
for p in inv.cleanup_paths:
p.unlink(missing_ok=True)
This support is marked "partial" in describe_capabilities because the schema is file-mediated rather than passed inline. Direct ll-orchestration call sites (evaluators.py, worker_pool.py) do not pass json_schema to build_blocking_json, so cleanup_paths is always () in those paths.
Separately, the FSM evaluators append an inline --json-schema flag directly to the invocation args, but only when the resolved host advertises HostCapabilities.structured_output (ENH-2627). Codex reports structured_output == "unsupported" — the file-mediated --output-schema bridge above is a different mechanism the evaluators do not use — so on Codex the evaluators skip the inline flag and rely on prompt-and-parse (with the BUG-2626 <StructuredOutput> tag fallback).
Hook intents without consumers¶
stop, post_compact, and permission_request events fire in Codex but have no little-loops consumer today. These hooks are intentionally absent from .codex/hooks.json.
See also¶
- Getting Started — install, trust prompt, skill discovery
- Host Compatibility Matrix — authoritative per-host feature matrix
- Troubleshooting —
HostNotConfigured, hook trust, binary detection - Codex adapter source — transport shims and trust-model details