Skip to content

codex-in-claude v0.19.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 00:47
· 25 commits to main since this release
13640e4

A sandbox-hardening and egress-disclosure release.

Three workspace-write guarantees that were advertised but not enforced now hold at every
isolation level: network egress and the writable-root set are pinned on the command line, where
the user's own $CODEX_HOME/config.toml can no longer widen them, and codex exec --strict-config turns a pin codex silently ignores into a zero-spend startup failure instead of
a guarantee that quietly stopped holding.

The egress disclosures were corrected in the unsafe direction they were wrong in. Codex's reads
are not bounded by the workspace; AGENTS.md is auto-loaded from the repository root and from
$CODEX_HOME as well as from the resolved workspace; a discovered skill's body reaches OpenAI
through a read the model itself issues after selecting it on its description alone; a dry-run
preview does not bound what the paid call sends; and the propose tier's throwaway worktree does
not bound where Codex writes, because workspace-write grants the OS temp roots by default.
Every one of these was probed live on codex-cli 0.148.0 with negative controls; none is a
behavior change, and each corrects a claim that never held.

Foreign text is no longer echoed raw. A rejected config key, codex's stderr, an unknown argument
name, a job id, and the codex --version string are now sanitized, bounded, or rejected before
they reach a result envelope, so a control character can neither spoof a terminal nor wedge
itself into a secret to defeat the redactor.

The tracked Codex version moves to 0.148.

Three breaking changes: an unknown key in your own Codex config now fails pin-carrying runs; the
propose-tier surface retracts its "writes only inside a throwaway worktree" promise; and five
parameters reject control characters in their values. The agent-visible surface changed nine
times (result fingerprint codex-in-claude/0.1/schema-75schema-84, and the persisted
RESULT_FORMAT 810), so pre-1.0 this is a minor release; clients that cache by
fingerprint re-fetch the contract, and a reader older than RESULT_FORMAT 10 rejects newly
stored job records.

Changed

  • BREAKING: codex exec --strict-config guards the guarantee-bearing -c key pins, and a new
    user_config_rejected error code reports an unknown key in your own Codex config.
    codex
    silently tolerates an unknown config key, so an upstream rename of
    sandbox_workspace_write.network_access (#518), sandbox_workspace_write.writable_roots
    (#520), or model_reasoning_effort (#309) would leave the plugin sending a key codex no longer
    reads — reopening the guarantee with no signal. The flag turns that into a zero-spend startup
    failure, since config parsing precedes auth and any model call. It is emitted only on runs
    that carry a -c override
    — every workspace-write run, every effort-carrying run, and any
    run with an operator -c in CODEX_IN_CLAUDE_EXTRA_ARGS — because at the default inherit
    isolation the flag also hard-fails on an unknown key anywhere in the user's own config,
    including tables for profiles the run never selects; a plain read-only consult carries no pin,
    so sending it there would risk availability while guarding nothing.

    Breaking because a previously accepted operator environment — a junk or version-skewed key in
    $CODEX_HOME/config.toml — now hard-fails pin-carrying runs. Ownership keys on the rejected
    KEY, never on the shared -c descriptor codex's own message contains: a rejected plugin pin is
    cli_contract_changed (the fail-loud drift signal this exists to produce), an operator key or
    an operator --profile-selected file is extra_args_rejected, and any other config file is
    the new user_config_rejected — permanent, repairing with correct_config, naming the file
    and line to fix and offering isolation="ignore-config" only as an explicitly lossy fallback,
    since it drops your entire config. Neither codex_status nor a dry run parses Codex config, so
    neither can predict this failure. Strict validates key names only, so a bad
    reasoning_effort VALUE still takes the backend invalid_reasoning_effort path unchanged.
    (#524)

  • BREAKING: the propose-tier surface no longer promises "writes only inside a throwaway
    worktree", and the delegate tools now advertise destructiveHint: true.
    codex's
    workspace-write sandbox grants the OS temp roots (/tmp and $TMPDIR) by default
    (exclude_slash_tmp/exclude_tmpdir_env_var default false), verified live at this plugin's
    exact argv, so the exclusivity claim never held. The exclude_* keys are deliberately not
    pinned closed — build tools, uv, git, and test runners need $TMPDIR — so every carrier
    discloses the grant instead: a canonical cli_contract.WORKSPACE_WRITE_SCOPE_FACT is carried
    verbatim by both delegate descriptions, their codex_capabilities returns entries, and a new
    negative_scope item, each alongside the persistence clause (temp-root writes are neither
    captured in the returned diff nor cleaned up); Meta.tier and Meta.sandbox carry a short
    form. The annotation preset splits: MCP defines destructiveHint: false as "performs only
    additive updates", and a delegated task can overwrite or delete pre-existing files under the
    temp roots, so codex_delegate/codex_delegate_async flip to true while consult and review
    keep false — the split is stated on annotations_reading.

    Breaking because the retired carriers made an exclusive promise ("writes only inside", "the
    worktree bounds what Codex may WRITE"), and correcting them weakens a documented guarantee
    a client could have relied on — even though runtime behavior is unchanged and the guarantee
    never held. What remains true and promised: the plugin never applies anything to your working
    tree, and the returned diff is gathered from the worktree only. (#523)

  • The egress disclosures no longer scope Codex's reads to the workspace or the repo. Several
    descriptions said Codex reads "files ... from its resolved working dir", "other repo files",
    or "tracked files in the throwaway worktree". A reader takes that as a bound; it is not one.
    --sandbox read-only bounds writes: probed on codex-cli 0.148.0 under this plugin's own
    flags with the workspace a bare non-repo directory, the model shelled out and read a file in
    $HOME on both tiers, while a write attempt in the same read-only run was refused — the
    negative control that makes the read informative. A canonical cli_contract.READ_SCOPE_FACT
    now states the truth at every carrier: the server instructions block, the codex_status
    caveat, codex_capabilities' negative_scope, all six active tools' returns and docstrings,
    readonly_honesty_statement, the extra_context parameter contract, and workspace_root's
    own description, which now says it selects where Codex works, not what it can read. It is a
    ceiling, not a warrant — the probe shows reads far outside the workspace, not that every
    file the OS user can read is reachable, since a platform sandbox can still deny individual
    paths. Not breaking: no carrier ever promised reads were confined (every one said "may read"),
    so this widens a disclosure. The practical consequence for callers: narrowing workspace_root
    is no longer a mitigation for egress.

  • The egress disclosure names every AGENTS.md Codex auto-loads, not just the workspace's own.
    Two further sources reach the model, both previously understated: inside a repository the load
    walks up from the resolved workspace to the repository root, so a caller who narrows
    workspace_root to a subdirectory precisely in order to bound egress still ships the repo-root
    AGENTS.md — a file it never named (#472); and a user-global $CODEX_HOME/AGENTS.override.md,
    else $CODEX_HOME/AGENTS.md, loads on every call from any workspace — the AGENTS.md twin of
    the $CODEX_HOME/skills/ hole (#358). Neither --ignore-user-config nor
    -c project_doc_max_bytes=0 suppresses the user-global file, though the latter does suppress
    the workspace and ancestor files. Outside a repository there is no walk at all.
    cli_contract.SKILLS_DISCOVERY_FACT and every carrier state all three sources. Not breaking.

  • Every disclosure now states HOW a skill's body reaches OpenAI, not just that skills are
    discovered.
    The two halves of Codex's implicit context arrive differently: AGENTS.md
    content is auto-loaded, already in context before the turn, while a skill is
    auto-discovered as name and description only, and its body follows a read the model
    itself issues once it selects that skill (#480). The wire and the prose now agree on this, in
    one canonical sentence (cli_contract.SKILL_BODY_FACT) across the server instructions block,
    the codex_status caveat, negative_scope, all six active tools' returns and descriptions,
    SECURITY.md, README.md, COMPATIBILITY.md, the collaborating-with-codex skill and its
    fallback reference, and the upgrade docs. Four wire carriers gained disclosure rather than
    wording: codex_consult_async and codex_review_changes_async named the skills roots with no
    mechanism at all, as did both delegate returns. The warnings are stronger, not weaker —
    each site now says the metadata alone is enough for the model to select a skill, and that
    selecting it pulls the body to OpenAI even when the prompt names neither the skill nor the
    file. Not breaking. (#498, #501)

  • The dry-run previews no longer imply they bound what a paid call can send. codex_dry_run
    led with "Preview what a codex_review_changes call would send", so a clean preview read as
    "nothing sensitive will be sent". It cannot establish that: a dry run never invokes Codex, so
    the model's own reads have not happened yet and none can be listed, and those reads are not
    bounded by the workspace. A canonical cli_contract.PREVIEW_SCOPE_FACT states the limit at
    both dry-run descriptions, their two codex_capabilities entries, a new negative_scope item,
    and the server instructions block. It is deliberately scoped to the model-initiated channel
    — a preview genuinely does bound the half this plugin assembles, enforcing max_input_bytes
    and reporting truncation — so an unscoped claim would have retracted a true guarantee. Both
    dry-run descriptions also now carry READ_SCOPE_FACT, which they never had. Not breaking: no
    carrier promised a preview was exhaustive, and the sibling redaction clause already disclaimed
    exactly this reading.

  • Tracked Codex version is now 0.148. SUPPORTED_VERSIONS tracks (0, 148); a 0.147 CLI
    now warns as untracked (advisory only — it never blocks). docs/UPGRADING-CODEX.md was run end
    to end against codex-cli 0.148.0 and A/B'd against a side-by-side 0.147.0. No contract
    drift:
    all 11 ALWAYS_SEND_FLAGS, the --model help-gated flag, and the three --sandbox
    values are present; the guarantee semantics were re-probed live (read-only blocked a write,
    workspace-write allowed a workspace write while blocking network egress,
    --output-last-message received the final message, --output-schema output conformed); and
    the three CONTRACT_DRIFT_STDERR_PATTERNS still match verbatim. Alongside it:

    • KNOWN_MODEL_SLUGS drops gpt-5.6-sol-wm, refreshed from the 0.148.0-written
      models_cache.json. What was observed is only that the slug is absent from the
      backend-served catalog cached by 0.148.0; no contemporaneous 0.147 observation was taken,
      so this record does not establish that the removal is independent of the new client. The
      reasoning-effort discovery fields still hold their pinned shape.
    • New 0.148.0 surface is deliberately not adopted: the migrate-rollouts and codex exec fork subcommands and ten new feature flags, none of which this plugin sends or reads. codex exec gained no new flag. remote_plugin is still stable/default-on, so the unconditional
      --disable remote_plugin isolation stays load-bearing.
    • The 0.147.00.148.0 app-server schema diff leaves every consumed schema byte-identical
      after canonicalization; three unconsumed v2 messages were added and none removed.
    • The implicit-context probe was re-run against both binaries and the presence matrices are
      identical to each other and to the 0.147 record: a $CODEX_HOME/skills/ skill is
      discovered despite --ignore-user-config, .claude/skills/ is not, and a parent AGENTS.md
      above the git root is not loaded.
  • The posture on Codex's default-on view_image feature is recorded: left enabled,
    deliberately
    (#479). It is a model-invoked tool taking a filesystem path, not implicit
    context — a zero-spend codex debug prompt-input A/B with a positive control found no image
    auto-attached and no difference under --disable view_image. Disabling it would buy no
    containment, since the read-only tiers already give the model a shell over the same filesystem;
    it would only stop the model interpreting pixels, and unlike remote_plugin it opens no
    channel outside the sandbox. The assumption the decision rests on — that the native handler
    enforces the same read boundary as shell execution — is verified against the rust-v0.148.0
    handler (#507). The published disclosures stay modality-neutral, so nothing agent-visible
    changes.

  • Documentation and internal conventions. Safety-critical rules in the egress-caveat docs are
    no longer buried in narration — README.md's Safety bullet is split with "do not target a
    workspace containing secrets you cannot disclose" promoted to a bolded lead, SECURITY.md's
    compound bullet becomes one facts bullet plus an imperative per obligation, and the fallback
    reference's abort rule joins the rule list it belongs to; placement only, no warning weakens.
    The COMPATIBILITY.md implicit-context probe was rebuilt to prove the model did not read
    the markers itself rather than asking it to say so: it forbids shell commands and file reads,
    keeps every codeword and synthetic skill name out of the prompt, and is captured with codex exec --json so the operator can assert over the run's own event stream, with a failure treated
    as inconclusive rather than negative. A new unprompted-selection consult is what actually
    establishes the egress claim: with a global skill whose description matches an ordinary task, a
    prompt naming neither the skill nor the file nor the codeword still brought the body back.
    docs/UPGRADING-CODEX.md's two-binary A/B loop uses the same instrument, so following the
    upgrade procedure can no longer reproduce the defect this fixed. AGENTS.md gains a Package
    boundary
    section — the single home for deciding whether a change belongs in this bridge or in
    upstream pontonier — and CONTRIBUTING.md's partial restatement becomes a link to it.
    COMPATIBILITY.md no longer repeats the recommended_plugins paragraph, and a repo-wide guard
    now scans every Markdown file for a verbatim-repeated prose paragraph, since every existing doc
    guard is presence-based and a presence check passes at one occurrence and at ten.

Fixed

  • The workspace-write guarantees can no longer be widened through the config-file channel.
    At the default inherit isolation the plugin sends no --ignore-user-config, so codex reads
    the user's $CODEX_HOME/config.toml — where two settings that are perfectly reasonable for
    someone's own interactive codex use silently widened the plugin's advertised bounds.
    [sandbox_workspace_write] network_access = true re-granted the delegate tiers full network
    egress
    (#518), and writable_roots = [...] let a delegate write outside its throwaway
    worktree
    (#520). The -c denylist refused sandbox_* keys on the argv passthrough channel,
    but nothing inspected the config file. Both were verified live on codex-cli 0.148.0 under the
    plugin's exact default flag set, with positive controls (curl reached example.com; writes
    landed at pre-absent targets outside the worktree). Every workspace-write run now pins
    -c sandbox_workspace_write.network_access=false and
    -c sandbox_workspace_write.writable_roots=[], both verified to outrank the config file and
    an operator --profile, so the advertised promises hold at every isolation level and the
    --profile operator-trust carve-out is closed for these two keys. codex's own implicit grants
    (the worktree itself, and the temp roots disclosed above) are unaffected, so default-config runs
    behave as before. No fingerprint change — the pins alter only the built argv, and the
    agent-visible text already asserted the guarantees this makes true. COMPATIBILITY.md
    discloses both pins, and docs/UPGRADING-CODEX.md gains the semantic probes that guard them
    against silent upstream key drift, since codex ignores an unknown -c key rather than
    rejecting it.

  • BREAKING: foreign text is sanitized, bounded, or rejected before it reaches a result
    envelope.
    Every path that quoted foreign text into an envelope echoed it raw: codex's stderr
    on the generic nonzero_exit branch, the codex_transfer diagnostics including
    app_server_stderr_tail, the review path's raw-output preview, stored job-result fragments,
    exception text, config keys and profile names, the CODEX_IN_CLAUDE_EXTRA_ARGS descriptors and
    refusal messages, the catalog strings codex_models reads from $CODEX_HOME, and the
    codex --version string (unbounded — a 5010-character version came back at full length). Two
    consequences: an escape sequence reaching a terminal can recolor, reposition, or erase — enough
    to spoof or hide surrounding output, and the text is attacker-influenceable, since a repository
    under review can make codex print chosen text — and a control character wedged into a secret
    defeats the redactor's patterns outright, so the value rides out as plaintext.

    The remedy splits by who can fix the value, because deleting control characters is the
    wrong answer for a machine field: it repairs a malformed value instead of letting it degrade.
    A verdict of pa\x07ss is not a valid verdict and must become unknown, but stripping turns
    it into an affirmative pass at high confidence; a job_id of abc<BEL><ESC>[31mdef came
    back as No job 'abc[31mdef', naming an id the caller never sent.

    • Rendered prose is sanitized. summary, questions, assumptions, next_steps, a
      finding's title/evidence/risk/recommendation, error.message, repair.alternative,
      and app_server_stderr_tail — on consult, review, and delegate alike, on fresh and replayed
      results. Single-token spans (a config key, a path, a rejected flag name) are bounded and
      single-line; multi-line diagnostics keep line breaks wherever that is provably as safe, since
      a rolling stderr tail exists to be read. Truncation is now marked (…[truncated],
      reserved inside the budget) for every echoed span: a clipped config key that reads as a
      complete one sends the caller after a key they never wrote.
    • Caller-correctable inputs are rejected at the MCP boundary, at zero spend, reporting the
      static parameter NAME and never the value: job_id, base, commit, model, and
      transcript_path advertise config.CONTROL_CHAR_FREE_PATTERN in their own inputSchema,
      following reasoning_effort, which already worked this way. This is the breaking half
      five parameters narrow their accepted value set.
    • Identity-bearing values are preserved byte-exact: cwd, candidate_roots,
      Finding.file, session_id, source_path, verdict, severity, and the --version string
      as codex.codex_version() returns it to config.parse_version. POSIX permits a control
      character in a filename, so such a value may be entirely correct, and a
      repair.arguments.workspace_root naming such a repository still round-trips. Sanitizing at
      capture would repair identity text — deleting the control character out of 0.<BEL>148.0
      yields a plausible codex-cli 0.148.0, turning a version that does not parse into one that
      does — so codex_version and version_warning can legitimately disagree, and
      codex_version's description now says so. version_warning is now static: it never
      quoted anything codex_version did not already carry, so interpolating it only copied
      untrusted text into a second sink.
    • An unknown argument NAME is withheld, the one site no schema can cover, since it is
      rejected precisely because it is unknown: details.field and invalid_arguments[].field
      carry a fixed marker plus a new field_withheld: true. The flag is the machine
      discriminator — the marker is not a reserved word, so a caller may genuinely send an argument
      named <withheld>. Detection runs on the raw location components, before the length bound
      truncates, so a control character past the bound cannot be cut away and the remainder
      reported clean.

    Two carriers no client sees as prose are deliberately left alone: raw_response, diff, and
    meta. codex_models' slug is left alone too — it is the identifier, and it is
    pattern-validated. error.resource_uri needed nothing: ReadResourceRequestParams.uri is a
    pydantic AnyUrl, which percent-encodes any raw Cc before the field is populated.

    The persisted RESULT_FORMAT moves to 10: InvalidArgument and ErrorDetail are closed
    schemas (extra="forbid"), so an older reader would reject a stored envelope carrying
    field_withheld. The text-sanitizing mechanism is upstream (pontonier 0.6.0), since it is
    CLI-agnostic; which spans are echoed, each one's bound and truncation direction, and which
    variant a span gets are bridge policy and stay here. One related leak is fixed at the source
    rather than the sink: a control character in a printed worktree path defeated the alias
    matching that suppresses dead absolute paths, so the path survived — a second, independent leak
    of the #420 guarantee that no amount of stripping the OUTPUT could fix. The
    argument-validation LOG still echoes a rejected value, tracked as #532. (#528, #529, #531)