Root context lacks a cumulative hygiene boundary across self-state reads and lossy tool output

Open 💬 1 comment Opened Aug 24, 2026 by gutocarollo

Summary

Codex root sessions currently lack one cumulative context-hygiene boundary across two already reported behaviors:

  1. the root can read its own persisted rollout/SQLite state and re-inject it as fresh tool output; and
  2. individually bounded tool results can still accumulate, while native truncation provides no protected, recoverable artifact reference.

I reproduced the interaction on Codex CLI 0.149.0 and built a best-effort local mitigation. The mitigation reduced input for a controlled fixture and blocked tested direct self-state forms, but it also demonstrates why the complete fix must be native: shell-string matching is bypassable, PostToolUse runs after any native truncation, and replacement currently surfaces as a tool error.

Component and environment

  • Component: Codex CLI history/context manager, unified exec/tool results, session persistence, and hooks
  • Codex CLI: 0.149.0
  • Subscription: ChatGPT-authenticated; the exact tier is not exposed by the CLI
  • Platform: Darwin 25.5.0 arm64 arm (macOS 26.5.2)
  • Terminal: Apple Terminal 470.2
  • Public source inspected: tag rust-v0.149.0, commit 758ef40f50c1a458425c7cfbf1eb12cbc07af0b0
  • Observed root model: gpt-5.6-sol
  • Controlled E2E model: gpt-5.6-luna

codex doctor --json was available and reported:

{
  "schemaVersion": 1,
  "overallStatus": "warning",
  "codexVersion": "0.149.0",
  "config.load": "ok",
  "installation": "ok",
  "runtime": "standalone on macos-aarch64",
  "state.paths": "inspectable",
  "state.rollout_db_parity": "ok",
  "warning": "low disk space; unrelated to the reproduced context semantics"
}

Version 0.149.1 became available after the evidence capture. This report is intentionally scoped to the verified 0.149.0 environment rather than claiming an untested version.

Related issues

  • #27131 reports recursive self-ingestion from session JSONL.
  • #14206 requests recoverable auto-spill for large tool output.

This issue is the cross-cutting invariant exposed by their interaction: the root should receive bounded evidence references, not unrestricted self-state or an unbounded cumulative sequence of individually truncated exploratory results. If maintainers prefer to track that invariant entirely in either existing issue, this report can be closed as a synthesis after its evidence is linked there.

Reproduction and evidence chain

1. Fixed rollout and SQLite checkpoint

At immutable rollout ordinals 0..575:

| Measurement | Value |
|---|---:|
| JSONL records | 576 |
| JSONL bytes | 4,441,768 |
| SQLite projection next byte | 4,441,768 |
| SQLite projection next ordinal | 576 |
| Normalized history items | 193 |
| Combined measured tool output | 752,727 UTF-8 bytes |
| Last request input | 271,744 tokens |
| Last request cached input | 269,056 tokens |
| Reported model window | 828,400 tokens |
| Cumulative thread processing | 15,073,225 tokens |

The fixed JSONL prefix hashes to f10f8d18005c75c6d5ae82a1dcfcfeadbeed4d0f9dc221114fbd5a52b2078441. The SQLite projection later advanced, so the historical byte/ordinal equality is reported as a timestamped observation rather than a permanently re-queryable fact.

Sanitized proofs:

2. Cumulative growth

A later sanitized snapshot contained 343 model-facing tool outputs totaling 1,190,101 characters. Between its first and last token samples, input grew by 474,909 tokens; the last sample occupied 61.0692% of the reported context window.

No single output needs to exceed its native cap for repeated exploratory results to consume substantial root context.

3. Recursive self-state path

The root can invoke ordinary shell tools against $CODEX_HOME/sessions/**, archived sessions, rollout JSONL, and local history SQLite files. The returned bytes are then ordinary tool output eligible for root history, creating this feedback loop:

inspect context growth
  -> search $CODEX_HOME
  -> match the current or historical transcript
  -> print transcript bytes as tool output
  -> include bounded result in later root input
  -> investigate the additional growth

This extends #27131 with SQLite projection evidence and with the cumulative effect of repeated individually bounded outputs.

Controlled mitigation experiment

I ran three alternating baseline/guarded pairs. Both arms used the same prompt, requested command contract, model, working directory, hook feature, nonce-bound temporary workspace, one successful command, byte-identical command output, and final OK. Context Mode was disabled in both arms. Only the temporary max_inline_bytes policy changed.

Results:

  • baseline artifacts: 0/3;
  • guarded artifacts: exactly one private, SHA-256-verified, non-pretruncated artifact in each run;
  • input-token pair deltas: 7,298, 7,313, and 7,306;
  • fixture mean avoided input: 7,305.67 tokens (10.1192%).

The percentage is specific to this approximately 15 KB synthetic fixture and is not claimed for general workloads. The functional result is that the guarded path preserved the requested outcome while replacing the model-facing payload with a compact receipt.

Actual behavior

  • Persisted Codex session state is readable through ordinary root tools.
  • Repeated bounded results accumulate in later root prompts.
  • Truncated bytes do not receive a stable protected artifact contract.
  • A local PostToolUse replacement appears as a tool error, and it cannot recover bytes removed before the hook runs.
  • A generic artifact path can itself become another self-ingestion surface if broad root searches rediscover it.

Expected behavior

Codex should enforce a native context-hygiene invariant:

  1. recognize the active session's own rollout, history projections, and protected artifact store;
  2. require explicit approval or metadata-only access when the root tries to read those sources;
  3. apply a cumulative root exploration budget before constructing the next sampling request;
  4. convert oversized or over-budget results to a typed artifact envelope before model-history insertion;
  5. expose bounded range/search/structured-read operations by opaque artifact ID;
  6. keep persisted audit evidence separate from model-visible content;
  7. bound successful subagent completion forwarding independently of raw child exploration.

This is an expected-behavior contract, not a claim that the local Python API is the correct upstream implementation.

Public-source causal anchors

The tagged public source shows the relevant flow:

These anchors support the causal interpretation for the public tag. I cannot prove byte-for-byte correspondence between the distributed binary and every closed service.

Privacy

The repository intentionally excludes raw rollouts, prompts, full tool output, SQLite item_json, credentials, personal paths, and thread IDs. Public artifacts contain aggregates, hashes, redacted paths, and query descriptions only.

Limits of the local mitigation

The local hook is intentionally fail-open and is not a security boundary. Shell aliases, variables, command substitution, interpreter code, unrecognized tools, malformed payloads, or hook-schema changes can bypass known-form matching. It has no automatic artifact TTL/quota, cannot cover every hosted tool path, and does not establish cross-platform or closed-service behavior.

View original on GitHub ↗

1 Comment

gutocarollo · 3 days ago

Validated mitigation and implementation

The problem is a form of recursive context self-ingestion.

Codex harness behavior

  1. Codex stores sessions, rollout JSONL, logs, and conversation history inside CODEX_HOME, normally ~/.codex.
  2. The root agent can directly execute a command such as:
rg -n "model" ~/.codex -g '*.json*'
  1. The harness executes the command and inserts its raw stdout into the root agent's conversation context.
  2. Previous conversations and tool outputs can therefore re-enter the current session as a new tool output.
  3. The native harness does not currently recognize or contain this self-referential search. Containment after execution is too late once those bytes have already entered the conversation.

In the reconstructed evidence, the correlated exploratory results contributed 752,727 measured UTF-8 bytes. A later checkpoint contained 343 tool outputs, 1,190,101 output characters, and 474,909 additional input tokens. This is correlation evidence, not a claim that every additional input token was caused by those outputs.

Implemented mitigation

A focused Context Mode PR implements a deterministic Codex-specific PreToolUse control:

  • It detects direct rg, grep, and find commands issued by the root agent when a path operand is exactly the effective CODEX_HOME root.
  • It stops the command before execution.
  • It redirects the operation to ctx_execute, which retains filesystem access under the host permission model while allowing only selected aggregates, hashes, redacted paths, or bounded results to enter the conversation.
  • It preserves normal access to pointed files such as ~/.codex/config.toml.
  • It preserves child-agent exploration.
  • It passes the command through when Context Mode MCP is unavailable.
  • It supports POSIX and Windows paths, relocated CODEX_HOME, absolute executables, leading environment assignments, and common shell wrappers.

The hook performs the enforcement; the redirect message only tells the agent how to retry the operation safely.

In the live E2E verification, the root-agent command was stopped before execution and produced no command_execution event. The equivalent bounded child-agent command remained available and returned CHILD_EXECUTED.

This closes the specific broad-search escape through CODEX_HOME for Context Mode users. It is an integration-level mitigation, not a replacement for a native Codex harness fix, and it does not claim to contain every possible large native tool result.

Implementation and evidence