Problem
Three separate ways Codex tracking can look broken, each with a real, shipped cause:
-
Codex usage shows $0 even though you've been running
codexdaily — usually on a WSL machine where Codex was only ever launched from a plain Windows terminal (cmd/PowerShell), not from inside WSL. -
The dollar total looks wrong for a session where you know you switched Codex
models mid-way (e.g. from a lighter variant to
gpt-5.4for a harder turn) — the cost doesn't move the way you'd expect from the switch. -
Codex sessions never show up in the MCP/tool breakdown —
token-meter stats's "MCP & tools" table and thesession_toolsMCP tool look empty or Claude-Code-only, even for a Codex session you know called several tools.
Cause
Each of these traces to a specific, documented gap between how Codex writes its JSONL logs and how Claude Code does — not a generic "Codex support is worse" hand-wave:
1. Session directory discovery (WSL/Windows)
codexSessionsDir() resolves to ~/.codex/sessions under Node's
homedir(). On native Windows or macOS/Linux that's the only place Codex writes, so it's
fine. On WSL, though, a Codex CLI that was only ever run from a plain Windows
terminal writes to /mnt/c/Users/<profile>/.codex/sessions — a path a WSL-side scan
of ~/.codex/sessions never sees, so every real session there ingested $0, silently.
codexSessionsDirs() (src/codex-ingest.ts) now mirrors the same fallback
Claude Code project discovery already used: it scans the WSL home path and every
/mnt/c/Users/<profile>/.codex/sessions that exists, via
scanWindowsUserDirs() in src/platform.ts. That helper deliberately checks
which directories actually exist rather than guessing a Windows username — USERPROFILE
is often unset under WSL, and the first entry under /mnt/c/Users can be a sandbox/system
account (e.g. CodexSandboxOffline), not the real user.
2. Per-turn model-switch pricing
Codex's JSONL log includes a turn_context entry ahead of each turn, carrying the model
actually serving that turn in payload.model. Before this was fixed,
codex-parser.ts read the model once from session_meta's
base_instructions text at the top of the file and billed every
token_count event in that session at that one rate — so a session that switched models
mid-way (e.g. gpt-5.3-codex-spark → gpt-5.4) had every turn after the
switch priced at the stale model's rate. The parser now tracks a currentModel variable,
updated from the latest turn_context.payload.model seen, and re-reads it before pricing
every token_count event — so pre-switch and post-switch turns land in separate,
correctly-priced rows. Logs from older Codex CLI versions with no turn_context entries
at all still fall back to the session-start model, because there's nothing else in the file to read.
3. Tool-call pairing
Codex logs a tool invocation as a function_call or custom_tool_call entry
carrying a call_id, followed later by a function_call_output /
custom_tool_call_output entry carrying the same call_id. Previously these
were ignored entirely — a Codex session only ever produced token_events (billing rows),
never tool_events — so the tool/MCP breakdown was structurally empty for every Codex
session, no matter how many tools it called. codex-parser.ts now holds each open call in
a pendingToolCalls map keyed by call_id and, when the matching
*_output arrives, emits a ToolEvent — response size and estimated tokens
from the output text (stringified via JSON.stringify when Codex emits a structured
object/array instead of a plain string, so the size isn't silently undercounted as 0), latency as
output-timestamp minus call-timestamp. A call whose session ends before its output ever arrives is
simply never flushed — not guessed at, not counted as zero.
Command
Four real surfaces, all reading the local SQLite DB — nothing here calls out to OpenAI.
1. Re-ingest with --force after upgrading
Token Meter's ingest is incremental (mtime + size) — a file it already processed under an older
version won't be re-parsed just because the parser logic changed. If you upgraded across the
WSL-discovery / model-switch / tool-pairing fixes above, a plain token-meter ingest
won't backfill anything for files it already saw; you need one forced pass:
token-meter ingest --force
Expect a much larger files_scanned count for Codex than before if you're on WSL and any
sessions were only sitting on the Windows side — that's the previously-invisible data being picked
up, not duplication.
2. Scope the audit to Codex
token-meter audit --days 7 --source codex --json
--source accepts all | claude | codex (maps
internally to the codex source tag). Drop --json for the terse terminal
view, which includes the same per-source Warnings block. --days is clamped
to your tier's history cap (7d Free, 30d Pro).
3. Aggregate stats — Codex separates by model name
token-meter stats 7
stats has no --source flag of its own — it always covers every ingested
source. In practice Codex is easy to pick out: its rows use gpt-* model ids in the "By
model" table, and Codex tool rows always show - in the mcp column of the
"MCP & tools" table (see Limitations).
4. From inside your assistant, over MCP
Use token-meter usage_summary with scope "codex" for this week
The usage_summary MCP tool's scope parameter accepts
auto (default — filters to your current platform) | all | wsl |
linux | win | windows | codex |
claude-code. Ask "Use token-meter recent_sessions" for a list that includes
ready-to-paste codex resume <id> commands for Codex sessions specifically.
Synthetic output
Fabricated example numbers below — not real user data — to show the shape of each command's output.
token-meter ingest --force (WSL machine, first run after upgrade)
$ token-meter ingest --force
Claude Code: scanned 214, processed 214, +18420 tokens, +6110 tools in 1840ms
Codex: scanned 97, processed 97, +5310 tokens in 610ms
token-meter audit --days 7 --source codex (terminal)
TOKEN METER AUDIT — LAST 7 DAYS Sources codex: partial (312 records analyzed, 100% of window covered — see warnings) Confirmed usage Tokens: 214.6k total (in 89.1k, out 61.4k, cache 64.1k) Estimated cost: $2.1140 Efficiency signals Findings: 2 Cost associated: $0.6120 Confidence: medium 1. 1 short gpt-5.5 session with minimal output Source: codex Project: /home/you/projects/api-gateway Session: - Tool: - Evidence: gpt-5.5 is billed at $30/M output tokens — at or above this audit's high-cost bar of $25.00/M, derived from the current pricing table rather than a hardcoded model name. 8a2f1c9d-... (/home/you/projects/api-gateway): 94s wall-clock, 210 output tokens, $0.6120. Confidence: low Suggested action: Review whether every short session required this model. 2. apply_patch calls averaging 4.2s Source: codex Project: /home/you/projects/api-gateway Session: - Tool: apply_patch Evidence: Mean latency 4.2s (4200ms) across 6 call(s) with a recorded latency_ms, in the 7d window. p50 (median) 3.8s, p95 6.1s, max 6.4s. failure_rate is not available: tool_events has no error/failure/status column in this schema, so it is reported as null rather than fabricated. Confidence: medium Suggested action: Investigate whether apply_patch's output is actually used downstream; if not, drop the call or cache the result to avoid the 4.2s penalty per call. Warnings - [codex] cache_inefficiency (D5) does not run for Codex: Codex's usage payload never reports cache_write_tokens, so cache-waste-day detection is structurally unavailable for this source (see src/audit/detectors/cache-inefficiency.ts).
token-meter stats 7 — "By model" excerpt, same session as above, model switch mid-session
=== By model (7d) === gpt-5.3-codex-spark $1.5020 out=42.0k events=118 gpt-5.4 $0.6120 out=19.4k events=14 claude-sonnet-4-6 $12.3300 out=310.0k events=880
Interpretation
-
Sources → codex: partial—partialhere does not mean "something is missing/broken with your data." Per the audit's status rule, Codex is reportedpartialwhenever it has in-window data at all, precisely because at least one detector (D5, cache) is structurally unable to use Codex — see theWarningsblock for exactly which.availableis reserved for Claude Code, which has no such known gap. -
Two separate model rows in "By model" (
gpt-5.3-codex-sparkandgpt-5.4for what was, to you, one continuous work session) is the correct, expected shape post-fix — it'sturn_contextdoing its job, attributing pre-switch and post-switch turns to the model that actually served them instead of collapsing the whole session into one (wrong) rate. -
D6's
high_cost_model_signalfinding is a review prompt, not an accusation — it fires on objective thresholds (short session + low output + high-cost-tier model, ranked by$/M outputrather than a model-name allowlist) but confidence is capped atlowbecause "was this model choice actually wrong" is a judgment call the thresholds alone can't make. -
Tool: -andmcp_server: -on Codex findings — expected, not a bug in this specific report; see Limitations for why Codex tool calls never carry an MCP server name. -
The ingest counters —
Codex: scanned 97, processed 97— a jump infiles_scannedright after upgrading on a WSL box (relative to what you remember from before) is the Windows-side directory being discovered for the first time, not duplicate rows; each file still dedupes by its own(session_id, ts, request_id, model)unique index.
Actions
-
Codex showing $0 on a WSL machine? First confirm you're on Token Meter ≥ 0.1.26,
then run
token-meter ingest --forceonce. If it's still $0, check whether/mnt/c/Users/<your-profile>/.codex/sessionsactually exists on the Windows side — if Codex was run from a WSL shell all along, its logs are under the WSL home dir instead, and a $0 result there points at something else (Codex never actually run, or a permissions issue reading~/.codex/sessions). -
Cost looks off after a mid-session model switch? Cross-check the "By model" table
(or
usage_summary) for that day — you should see one row per model actually used, not one blended row. If you only see the pre-switch model, the session's JSONL predatesturn_contextsupport in your Codex CLI version; there's no way to retroactively recover per-turn attribution from a log that never recorded it. -
No Codex rows in the tool/MCP breakdown at all? Re-ingest with
--forcefirst — tool-event extraction for Codex only exists from 0.1.26 onward, so any Codex session ingested under an older version has token rows but zero tool rows until you re-parse the file. -
Comparing a Codex-heavy vs. Claude-Code-heavy week? Use
usage_summary scope="codex"andscope="claude-code"separately rather than eyeballing the combinedstatsoutput — the scope filter is the one built specifically to avoid mixing platforms/sources, and it also surfaces a "hidden: $X.YY" hint for what the current scope excluded. -
Chasing a slow or oversized Codex tool (e.g.
apply_patch,shell)? Group bytool_namealone, not(tool_name, mcp_server)— every Codex tool row hasmcp_server: null, so grouping by the pair silently collapses every distinct Codex tool that happens to share a name pattern into one bucket labeled the same as any of Codex's own built-ins.
Limitations
-
Codex tool calls are never attributed to an MCP server. Every Codex
ToolEventis stampedmcp_server: nullincodex-parser.ts— Codex's transcript format doesn't carry themcp__<server>__<tool>naming convention Claude Code uses, so there is currently no way to tell a Codex-side MCP tool call apart from one of Codex's own built-in function/custom tools. You get tool name, response size, and latency for Codex tools — never which MCP server (if any) served the call. -
Cache-write tracking is structurally unavailable for Codex (D5). Codex's
usagepayload only ever exposescached_input_tokens(mapped tocache_read_tokens);codex-parser.tshardcodescache_write_tokens: 0for every Codex row, so a Codex session can never satisfy thecache_write > cache_readcondition the cache-inefficiency detector (D5) looks for. A--source codexaudit will never return a D5 finding, by design — the report'ssources[].warningssays so explicitly rather than silently showing "no cache issues." -
Codex tool events never get a
file_ext. The extension-based binary-file trim heuristic (REPEATED_BINARY) readstool_events.file_ext, whichcodex-parser.tsnever populates for Codex rows — that field is extracted only from a Claude Codetool_useblock'sfile_path/path/notebook_pathargument. Codex rows fall back to (or are simply absent from) the older frequency-based heuristic instead. -
"Repeated calls" (D4) is the same timing/size proxy for Codex as for Claude Code, not
argument-level dedup. Neither source's
tool_eventstable stores call arguments by design (seeToolEventinsrc/types.ts) — Codex's tool-call pairing adds response size and latency, but not arguments. D4 clusters by same session + tool + timing proximity (5-minute window) + response-size similarity (within 10% of the cluster average) as a proxy for "probably did something similar repeatedly," never proof of identical inputs. Confidence is capped atmediumfor exactly this reason. -
An unmatched pending tool call is silently dropped, not counted. If a Codex
session ends (crash, Ctrl-C, terminal closed) while a
function_callis still awaiting its*_output, that call never becomes aToolEvent— it's neither estimated nor guessed at. A session interrupted mid-tool-call will show fewer tool calls than it actually made. -
The WSL/Windows directory fallback only matters if Token Meter itself runs under WSL.
If Token Meter runs natively on Windows, macOS, or Linux,
homedir()already resolves to the right place and there's only one directory to scan — a $0 Codex result there is a different problem (Codex never run, or a permissions issue), not the discovery gap this guide covers. -
One Codex model's price is a placeholder.
gpt-5.3-codex-sparkhad no published API pricing on OpenAI's site as of 2026-07-12 (it was a ChatGPT Pro research preview at the time) —pricing.tsprices it at the plaingpt-5base rate with aTODOto correct it once official pricing ships. Any session using that specific model id will under- or over-estimate cost until that row is updated. -
Response size is estimated, not billed. Codex tool-response tokens use the same
~3.5 chars/token heuristic (
estimateTokensFromText()) Claude Code's parser uses — a local sizing signal, not the token count OpenAI actually charged for.