Skip to content

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):

  1. LL_HOST_CLI environment variable
  2. LL_HOOK_HOST environment variable (falls back to claude-code when absent)
  3. Binary probe: claudecodex → error

You can also set the host permanently in .ll/ll-config.json (or .codex/ll-config.json):

{
  "orchestration": {
    "host_cli": "codex"
  }
}

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