Skip to content

codex-in-claude v0.17.0

Choose a tag to compare

@github-actions github-actions released this 03 Aug 02:09
· 59 commits to main since this release
61244b1

A secret-redaction release. The inline redactor was rebuilt around merged candidate spans so no
matcher can strand part of a secret another one covers whole; its labelled-secret pattern now
reaches quoted-JSON and bracket-subscripted keys without scrubbing ordinary source out of reviewed
diffs; two quadratic patterns were bounded; and a replacement that may be incomplete now says so
instead of advertising a completeness it doesn't have. Alongside that, codex_review_changes and
codex_dry_run disclose how a diff was redacted — files withheld whole versus files sent with
values masked inline — codex_capabilities declares the targeted MCP protocol revision and its
readOnlyHint reading, and the dry-run tools warn when the call they preview may exceed the
synchronous deadline. The tracked Codex version moves to 0.146. One breaking change: a blank
question or task is now rejected before any spend. The agent-visible surface changed nine times
(result fingerprint
codex-in-claude/0.1/schema-66schema-75, and the persisted RESULT_FORMAT 78), so
pre-1.0 this is a minor release; clients that cache by fingerprint re-fetch the contract.

Added

  • coverage.redaction distinguishes withheld files from inline masks on
    codex_review_changes and codex_dry_run (#433). The redacted omission reason previously
    conflated two different things a secret-looking diff can trigger: a whole file dropped (its path
    itself looked secret-bearing, e.g. .env) versus a file sent with one or more values masked
    inline. The new optional RedactionSummarywithheld_paths, masked_paths, and
    inline_masks (markers actually emitted, after span merging, not raw pattern candidates) —
    splits them, counting only content that lands in the retained, byte-capped diff text.
    meta.redacted_paths and DryRunResult.redacted_paths/redacted_paths_count are unchanged:
    still the single backward-compatible union of both path lists, and still codex_delegate's only
    redaction disclosure. The field is optional (so a Coverage persisted before it existed still
    replays) and the redacted reason can fire without it — a legacy-shaped producer, or a
    disclosure dropped entirely by byte-cap truncation — but the field is never populated without
    the reason. FINGERPRINT schema-74schema-75; RESULT_FORMAT 78 for the persisted
    Coverage. Additive, not breaking.
  • codex_capabilities declares the targeted MCP protocol revision via a new
    protocol_revision field ("2025-11-25"), previously derivable only by inspecting the
    initialize wire response (#423). Its description points at the new
    docs/adr/0004-mcp-2026-07-28-migration.md, which records why this server stays on 2025-11-25
    (verified FastMCP/mcp SDK support, not any spec-granted support window) and the per-feature
    migration plan for the deprecated pieces already in use. FINGERPRINT schema-70
    schema-71; additive, not breaking.
  • codex_capabilities documents the server's readOnlyHint reading via a new
    annotations_reading field, previously stated only in source comments (#426). The hint tracks
    whether a call changes observable state that outlives the response (a job record, committed
    spend) rather than file I/O — which is why codex_consult, codex_review_changes, and
    codex_delegate are readOnlyHint: false even though consult and review never write files,
    while the two dry-run tools, which create no job record, stay true. Consistency guards assert
    the claim against the live tool annotations. FINGERPRINT schema-71schema-72; additive,
    not breaking.
  • codex_dry_run and codex_delegate_dry_run advise when the previewed call may exceed the
    synchronous deadline
    via a new deadline_advisory field, described identically on both
    (#342). Non-null exactly when the previewed call would run the model and either its prompt
    exceeds 100,000 bytes or its resolved reasoning_effort is high/xhigh (an exact match — a
    near-miss like "Xhigh" is deliberately not treated as high); null whenever the call would run
    no model at all. It is a hint, not a refusal, and it names the previewed paid tool's own
    _async counterpart, never the dry-run tool's own name, which has none. FINGERPRINT
    schema-73schema-74; additive, not breaking.

Changed

  • Tracked Codex version is now 0.146. SUPPORTED_VERSIONS tracks (0, 146); a 0.145 CLI
    still runs and now warns as untracked (the gate is advisory and never blocks). The upgrade
    procedure was run end to end against codex-cli 0.146.0, A/B'd against a side-by-side
    0.145.0, and required no code change: help text for codex/exec/review/exec review is
    byte-identical, every guarantee-bearing flag and sandbox value is present, the drift signatures
    still match observed output, and the model catalog is unchanged. Semantics were re-probed rather
    than assumed — read-only still blocks writes, workspace-write still writes in-workspace while
    blocking network egress, --output-last-message still delivers, -c model_reasoning_effort is
    still applied, and structured output still conforms. Upstream additions were reviewed and none is
    consumed: a new app-server --help flag, two new app-server messages, an optional providerId
    and an ent26 PlanType value on the consumed schemas (the plan type is read as a free-form
    capped string, not an enum), and four new feature flags — including mcp_2026_07_28, still
    under development, which corroborates this release's decision to target MCP 2025-11-25.
    remote_plugin remains default-on, so the --disable remote_plugin isolation guarantee still
    applies and still fails closed.
  • The skills-discovery egress caveat now reads identically everywhere it appears (#427).
    Fifteen prose sites — the server instructions, the codex_status caveat, six
    ToolCapability.returns clauses, codex_capabilities' negative_scope, and six tool
    docstrings — stated the same fact (Codex auto-loads the resolved workspace's AGENTS.md and
    discovers skills in that workspace's .agents/skills/ and in the user-global
    $CODEX_HOME/skills/, and the plugin's isolation flags do not suppress any of it) in
    independently drifted wording. All now share one canonical sentence pair defined in
    cli_contract.py, already the single source of truth for every Codex CLI assumption. This is a
    wording convergence, not a byte-identical refactor — no carrier shared one sentence before — and
    no guarantee is removed or weakened; the three _async docstrings keep their deliberately
    lighter subset. tools/list grew 157 bytes, inside its existing budget. FINGERPRINT
    schema-72schema-73; not breaking.
  • Meta.timeout_seconds is now described — it was the only semantically-loaded Meta member
    published without a description (#413). It states which deadline the value is, by envelope: a
    synchronous paid call reports that call's own resolved deadline, post-clamp (10–600s); a
    background job's start handle, or a later codex_job_result fetch of that job's originating
    run, reports the job-lifecycle ceiling instead (job_max_seconds, default 1800, clamped
    60–7200); and invalid_arguments or internal_error, tied to no run, report the server's
    configured sync deadline. Wording only: FINGERPRINT schema-67schema-68,
    RESULT_FORMAT unmoved, not breaking.
  • The three sync tools' Progress & recovery paragraphs now describe job-record recovery
    correctly
    (#414). They named only codex_job_listcodex_job_result, omitting
    codex_job_status as the polling step in between, and framed recovery as a fallback for a
    dropped connection — when some MCP clients background a long tool call well before the server's
    own deadline. Wording only: FINGERPRINT schema-68schema-69, not breaking.

Fixed

  • BREAKING (agent surface): a blank question or task is now rejected before any spend
    (#411). An empty or whitespace-only value passed validation and bought a real Codex run that
    could produce nothing — the prompt builders strip it, so the model received framing scaffolding
    and no ask (a reported codex_consult(question=" ") burned 23,120 tokens and returned
    ok: true). codex_consult, codex_consult_async, codex_delegate, codex_delegate_async,
    and codex_delegate_dry_run now return invalid_arguments naming the offending argument, with
    a correct_arguments repair. Flagged breaking because it narrows an accepted value set: input
    that previously returned ok: true now returns an error. Blankness is Python's str.strip(),
    so U+00A0 is blank while U+200B counts as content, and the check sits after the byte-limit
    guard, so an oversize whitespace-only value still reports input_too_large. FINGERPRINT
    schema-66schema-67; RESULT_FORMAT unchanged.

  • Redaction no longer strands part of a secret behind a marker that looks complete (#443,
    #445). The inline patterns ran as sequential re.sub passes, each over the previous pass's
    output, and sub never revisits consumed text — so a matcher with a narrower value class could
    eat a prefix of a value a later matcher would have covered entirely: token=ghp_<20 chars>-tail
    came out as token=[redacted: secret value]-tail. The worse harm is not the disclosure but that
    the output advertised itself as fully redacted, suppressing the reaction a bare secret would
    provoke. Every pattern is now run with finditer against the original line, each candidate
    contributes the span it would replace, the spans are merged on strict overlap, and the line is
    rebuilt with one marker per merged interval — so the order of the pattern list no longer affects
    output at all. Touching (non-overlapping) candidates deliberately stay separate, and a preserved
    prefix (a labelled key, an Authorization: header, a connection string's ://user:) survives
    because it lies outside every merged span rather than because a replacement handed it back. The
    span projection fails closed: a candidate whose group 1 is not a leading, participating prefix
    of its match has its whole match redacted rather than the computed slice. One behavioral delta
    comes with this, in the fail-closed direction — the code-reference exemption is now judged
    against the original line, so a line whose evidence an earlier marker had erased is redacted
    instead of exempted.

  • A redaction marker no longer claims completeness it doesn't have (#446). A credential
    carrying a character the connection-string userinfo matchers exclude (/, or ?/# on the
    arms that stop there) can never be spanned by them, so span merging cannot help and an earlier
    matcher firing on its prefix still ships the remainder beside a marker:
    redis://u:token=[redacted: secret value]%2Fmore/tail@host looked complete while /tail went
    out in the clear. The rebuild site now decides per merged interval whether the replacement may
    be partial and emits [redacted: possibly partial secret value] instead. Two checks, either
    sufficient: the character right after the interval is not a safe terminator for that interval's
    trailing candidate type (a narrower set for labelled and Bearer candidates, whose value alphabet
    is not a specific credential's own grammar), or the interval's earliest whole-match candidate is
    preceded by a value-continuation character. Neither is a completeness proof — the plain marker
    means no affirmative sign of truncation was found, not that completeness was verified, which
    this best-effort mechanism cannot do.

  • The labelled-secret pattern now reaches quoted and bracket-subscripted keys (#432, #434). It
    required its :/= separator immediately after the label, so JSON's closing quote
    ("api_key": "…") and a subscript's "] (cfg["password"]["key"] = …) each defeated it
    entirely — silently exempting the carrier this pattern exists for: a credential with no
    recognizable vendor shape (AWS_SECRET_ACCESS_KEY, CI_JOB_TOKEN, an internal HMAC secret), in
    exactly the carriers that hold those — .json config, fixtures, captured API responses, and the
    raw_response.text this redactor is applied to. A value carrying a vendor prefix was caught
    anyway by its own pattern, which masked the gap. The label group now accepts an optional closing
    quote and a bracket subscript. Both widenings fail closed by construction: reaching either
    requires consuming a quote, and a match that consumed a key quote is never eligible for the
    code-reference exemption below — pinned by an invariant test, so moving the bracket out of the
    quote group fails CI even if every behavioral test still passes. The accepted cost is the mirror
    of that guarantee: ordinary source assigning to a key/token-ish subscript is masked out of a
    reviewed diff, including innocent suffixes such as obj["monkey"]. Measured rather than
    assumed — an A/B over 3,124 real source files found no line that stopped being redacted and
    three newly redacted, only one of them a genuinely new class.

  • A labelled match no longer swallows a later sensitive label's secret (#434, #436). The
    pattern's value run is greedy and re.sub never revisits consumed text, so an earlier weak
    label's value could absorb a later sensitive label whole and ship the real secret behind it in
    the clear: cfg "key": aaaa…password = realsecret1234567890 redacted the filler and left the
    secret untouched. Two guards now run in sequence, because no single formulation covered both
    shapes without reopening the quadratic risk below: the original guard for bracketed candidates,
    unbounded at any distance, and a new unconditional one covering non-bracket swallows — a shape
    never protected before, at any distance — bounded at 1024 value characters to stay linear on
    repeated-anchor input. Both share one refinement: a guard refuses only when the inner label's
    own value would itself clear the redaction threshold, so token = aaaa…key=short is not turned
    into a total miss. The accepted trade is that a refused candidate's own value run survives on
    the page and the engine advances to redact the inner label instead — a narrower span, not a
    leak. A non-bracket swallow chain whose gap exceeds the peek is exactly as unprotected as it
    always was, tracked as #465.

  • Secret redaction no longer scrubs ordinary source out of reviewed diffs (#421). The
    labelled-secret pattern matches any 16-or-more-character identifier run after a key/token-ish
    label, so plain code tripped it: token = _PLACEHOLDER_PREFIX + _placeholder_seed(text) reached
    Codex masked. Two harms compounded — the reviewer could not see the code it was asked to review,
    and because any inline mask makes coverage.status partial, the never-false-pass rule turned a
    model pass into unknown citing redacted, which reads as "this file had secrets" when one
    innocuous assignment tripped a heuristic. A labelled match inside a diff body line is now left
    intact when it is provably a code reference: the separator must be followed by whitespace (so
    api_key=value in config, env, shell, or a query string is never exempt), the label must not be
    from the password family — read across the whole logical label, so config.password.key = … is
    judged whole — the value must be an unquoted bare dotted identifier path, and it must be
    followed by a call, an operand, or, only with a : separator, an annotation default. The
    exemption applies only within a recognized source extension, because every one of those
    conditions is a claim about code syntax: in YAML the identical text is a plain scalar, and
    YAML nests the sensitive label on a preceding line, out of reach of any same-line test.
    Redaction is not weakened elsewhere — the exemption never applies to redact_text's arbitrary
    prose, and every vendor/JWT/PEM/connection-string pattern still runs on an exempted line.

  • A connection-string credential is redacted in every userinfo position it can occupy (#440).
    The matcher required a non-empty username, so redis://:pass@host went out verbatim — not an
    edge case but the canonical Redis URL, since Redis had no usernames before ACLs in 6.0 — and a
    token stored as the username with an empty password (https://<token>:@host, the
    token-as-username idiom) was never redacted either. Both are now matched, the latter at 16+
    characters, the credential threshold this module already uses. The password side stays required:
    an empty password holds no secret, and matching it would emit a marker claiming to have hidden a
    blank value. The bare ://token@host form is deliberately left alone at any threshold, because
    length cannot establish credential semantics in that position — a 16+ rule masks
    git+ssh://deployment-automation@git.example.com/repo and docker://prometheus-operator@sha256:…,
    identities rather than secrets, and raising the threshold only changes which identities get
    destroyed.

  • An ordinary URL whose query or fragment carries an @ is no longer masked as userinfo
    (#442). Per RFC 3986, ? and # terminate the authority component, so nothing after either can
    be userinfo — but the named-username matcher admitted both, on the password side and on the
    username side: https://host.example:8443?email=user@example.com (an email in the query, no
    userinfo anywhere) masked its port and query and partly hid its host, and
    https://host.example?foo:bar12345678@x.example (an ordinary query string carrying a colon)
    masked both slots. All three connection-string userinfo runs now derive their terminators from
    one shared exclusion set instead of three independently hand-spelled character classes. The
    accepted trade, pinned by a characterization test on each side: a password or username
    containing a literal ?/# — already invalid userinfo per RFC 3986 without percent-encoding —
    is no longer redacted. An old-versus-new sweep over 1,849,015 lines of third-party source found
    zero behavior changes in either direction, in both code-exemption modes.

  • Redaction is no longer quadratic on long or repeated-anchor text (#438, #439). Two patterns
    opened with an unbounded greedy run ahead of a literal that never arrives, so every start
    position scanned to the end of the run and then backtracked one character at a time: the
    connection string's scheme run ([a-zA-Z][\w+.-]*://) took ~15s on 100 KB of text, and the JWT
    first segment 5.7s on 160 KB of eyJ. This is a liveness concern rather than a disclosure —
    redact_text runs over untrusted model output, so input size and shape are attacker-influenced,
    and a synchronous call that blows its deadline is terminated with its paid work lost. The
    connection-string scan now simply starts at the ://, which costs no coverage (the scheme was
    never part of the replaced span, so output is byte-identical wherever the old pattern matched)
    and additionally recognizes userinfo whose :// no letter-led run reaches — closing a real leak
    on 0.16.0, where a preceding marker ate the scheme letters and key=<eaten>://user:pw@host
    shipped its password intact. The JWT first segment is bounded at 512 post-anchor characters,
    about 8x a realistic JOSE header; segments 2 and 3 stay unbounded, since capping the first also
    caps how many anchors ever reach them. Both are roughly linear after the fix and pinned by
    timing budgets.

  • codex_delegate's prose and error envelopes no longer point into the deleted worktree
    (#412, #420). Codex runs with its working directory set to the throwaway worktree, and that
    worktree is torn down before the caller reads the result, so every absolute path Codex wrote was
    dead on arrival — a reported run returned Created [HELLO_SMOKE_TEST.md](/private/tmp/cic-worktree-8t22rdm3/tree/HELLO_SMOKE_TEST.md), an
    invitation to open a file that no longer existed anywhere. Worktree-absolute paths — plus the
    file:// spelling and the symlinked-ancestor spelling (macOS resolves /tmp to /private/tmp)
    — are now rewritten to repository-relative form in summary and raw_response.text, and the
    delegate prompt asks for relative paths so the rewrite is a backstop rather than the only
    mechanism. Paths stay relative rather than re-rooted at the live repository on purpose: the
    diff is not applied, so a live absolute path would be equally dead for a new file and would
    point at differing content for an existing one. Error envelopes are covered too:
    classify_failure's nonzero_exit branch and the WorktreeError messages built from raw git
    argv/stderr now route through worktree.sanitize_prose, the one composition that relativizes
    and redacts in a single pass — the two operations reordered are each independently unsafe.
    diff was never affected; it already carried correct a//b/ paths.

  • invalid_arguments envelopes are now conformant wherever they are produced (#416, #418,
    #419). docs/REFERENCE.md promises an envelope with this code carries a per-argument list of
    {field, reason, allowed_values}, with details mirroring the first entry; codex_transfer's
    rejection of an invalid transcript_path and the untracked-policy rejection reached by a
    direct call both omitted it. The rule now lives in the one constructor every producer goes
    through, so a listless envelope is a loud programming error rather than a silently
    non-conformant result, and details is always derived from the first entry. The gitdiff
    branch's human-readable message also stopped echoing the rejected value: its machine fields
    already honored InvalidArgument's promise that a rejected value — which may be a secret — is
    never copied, while the prose beside them printed it verbatim. And the background worker now
    normalizes a nonconformant invalid_arguments envelope to internal_error immediately before
    the atomic write, so replay cannot return a never-conformant record stamped with the current
    fingerprint. Replay of records written before these rules is deliberately untouched.

  • initialize no longer advertises the io.modelcontextprotocol/ui extension (MCP Apps)
    (#424). FastMCP injects it unconditionally, so a host probing for MCP Apps support found the
    capability advertised with nothing behind it — this server implements no UI/Apps code. The
    existing get_capabilities seam now filters that one id, preserves any other entry a future
    FastMCP version might legitimately add, and omits the extensions key entirely only when
    nothing is left. Removing a falsely-advertised capability is a correction, not a removal of
    implemented behavior. FINGERPRINT schema-69schema-70; not breaking.

  • A transfer or rate-limit read's stderr_tail no longer races the drain thread that fills it
    (#449). _StderrDrain reads a child's stderr on its own daemon thread and kept no reference to
    it, so on a loaded runner an outcome could be assembled before the last, most diagnostic line
    ever reached the capture — and a quiesce placed in the finally could not fix it, since Python
    evaluates a return expression before finally runs. Every post-spawn exit in both functions
    now runs a shared settle step before constructing its outcome: terminate the child, join the
    stdout reader, give the newly-tracked drain thread a bounded (~2s) quiesce, then snapshot. A
    stream that never reaches EOF still yields a bounded, possibly incomplete tail — that boundary
    is deliberate and unchanged, and is now pinned by tests rather than left implicit.

  • COMPATIBILITY.md understated what auto-loads into Codex's context. Its implicit-context
    table recorded that an AGENTS.md above the git root is not loaded. It is — and re-probing the
    previous 0.145.0 binary shows the same, so this corrects a wrong earlier observation rather
    than a behavior change, in the unsafe direction. A negative control rules out model
    confabulation: removing the parent AGENTS.md makes its codeword disappear from the answer while
    the project one remains. Documentation only — the published egress caveat still says "the
    resolved workspace's AGENTS.md" and so remains narrower than observed behavior; widening that
    text carries its own FINGERPRINT bump and is tracked separately.