Observability

ArmorCodex and ArmorClaude Observability

Understand how ArmorCodex and ArmorClaude assemble, complete, and ship one trace per agent turn.

ArmorCodex and ArmorClaude both map one agent turn to one iap.plan trace, but they cannot use the same delivery design. ArmorCodex receives each hook in a new process; ArmorClaude normally keeps a daemon alive across hooks.

BehaviorArmorCodexArmorClaude
RuntimeNew short-lived process for every hookLong-lived daemon, with an in-process fallback
Turn stateAppend-only NDJSON on diskPer-session recorder registry in memory
Trace completionAssemble and ship during StopEnd the trace during Stop; background shipper sends it
Session cleanupStale-file sweep on a later SessionStartFlush and remove the recorder on SessionEnd
Typical visibilityAfter the Stop hook finishes shippingAfter Stop and the next background flush

Both integrations are additive and fail-open. A telemetry exception does not modify the hook's policy result. Boundary work is still real work: ArmorCodex awaits shipping in Stop, and ArmorClaude awaits final shipping in SessionEnd, so an unavailable endpoint can add latency at those boundaries.

ArmorCodex

ArmorCodex has no persistent observability daemon. A recorder created by one hook process cannot be resumed by the next process, so the bridge accumulates a turn on disk and constructs a fresh recorder only when it is ready to ship.

Turn lifecycle

UserPromptSubmit ── append trace metadata + plan-start span
PreToolUse / PermissionRequest ── append check + policy spans
PostToolUse ── append tool-report span
Stop ── rename, assemble, add token usage, ship, delete
  1. UserPromptSubmit creates the turn metadata and an iap.plan.start span. If a later hook arrives without metadata, Stop can synthesize a minimal trace record.
  2. PreToolUse and PermissionRequest append an iap.check span and a nested policy_call span. Allow and deny decisions are preserved.
  3. PostToolUse appends a tool.report span with ok or error status inferred from the hook payload.
  4. Stop claims the turn file, reconstructs the trace, adds per-turn generation usage from the transcript, and posts the complete trace once.

The bridge uses a file named for a SHA-256 hash of the hook session ID rather than placing the raw session ID in a path. The backend receives the session ID only when it is a valid UUID.

Append-only NDJSON

Each hook appends one self-contained JSON line to a per-turn NDJSON file. The file is opened with O_APPEND | O_CREAT | O_WRONLY, and each record is written in a single operation. This avoids the lost-update race of reading a JSON document, modifying it, and renaming a replacement while another hook process is doing the same.

Attribute objects are capped before serialization and every NDJSON line is limited to 3,800 bytes. If the process is killed during a write, assembly skips only the malformed final line and keeps other complete records.

Assembly and shipping at Stop

Stop first atomically renames the live .ndjson file to a .ndjson.shipping file. This gives the Stop process exclusive ownership of the claimed file before it reads anything. It then:

  1. reads the metadata and all complete span records;
  2. starts a fresh in-memory ObservabilityRecorder;
  3. replays the spans under a new recorder-owned trace ID;
  4. adds transcript token deltas as generation spans;
  5. ends the trace and awaits the POST to /observability/spans;
  6. removes the claimed file.

Because the hook process is short-lived, it cannot rely on the SDK's five-second periodic timer. The explicit flush is required for delivery. It also means an unavailable backend can make Stop wait for retries even though failures never change the policy outcome.

If the shipper exhausts its retries, the batch is dropped and the claimed file is removed. If the Stop process fails before cleanup, the .shipping file remains but is not treated as a durable retry queue.

Stale cleanup

A turn that never reaches Stop leaves an unshipped file. On a later SessionStart, ArmorCodex removes live or claimed observability files older than six hours. Partial stale traces are deleted rather than sent long after their original turn.

There is a narrow late-hook edge case: a hook that appends after Stop has renamed and shipped the prior file creates a new file at the original path. That record can be associated with a later turn or eventually removed by stale cleanup. The normal hook order completes tool hooks before Stop.

Enable or opt out

Observability is enabled when an API key is available. It uses the configured ArmorIQ backend base URL and sends product armorcodex.

The configuration bridge honors a forwarded disable_observability plugin option, but the current packaged plugin metadata may not expose it in a configuration UI. The environment variable below is the reliable customer-facing opt-out.

Set the opt-out environment variable before starting Codex:

export ARMORCODEX_OBSERVABILITY_DISABLED=true

Accepted true values include true, 1, yes, y, and on. If no API key is configured, ArmorCodex does not create observability records or make ingest requests.

ArmorClaude

ArmorClaude normally routes hooks through one long-lived daemon. The observability bridge keeps a module-level registry keyed by the hook session ID, so a recorder and active trace persist across the turn's hook events.

The non-daemon fallback is best-effort and process-local. It cannot provide the same cross-hook in-memory nesting as the daemon path.

Per-request effective configuration

Each hook forwards the current authentication and observability configuration to the daemon. The daemon creates an immutable effective snapshot for that request by overlaying those values onto its startup configuration. This prevents an old, environment-poor daemon configuration from silently disabling telemetry for new hook requests and avoids mutating shared configuration while sessions run concurrently.

The first event for a session creates its recorder from the effective request configuration. The registry correlates later events under the same hook session ID. Only UUID session and user IDs are sent in UUID-typed backend fields; the free-form agent ID is preserved.

Turn and session lifecycle

UserPromptSubmit ── start iap.plan trace
PreToolUse ── add check + policy spans
PostToolUse / PostToolUseFailure ── add tool-report span
Stop ── end trace for background shipping
SessionEnd ── end any active trace, flush, remove recorder
Daemon shutdown ── flush already-ended traces
  • UserPromptSubmit closes any prior active trace, starts the next iap.plan trace, and records a bounded prompt preview.
  • PreToolUse records allow, deny, or ask as a nested policy call.
  • PostToolUse and PostToolUseFailure record tool output with ok or error status.
  • Stop closes the current trace. Ended traces become eligible for the SDK background shipper, which flushes every five seconds by default.
  • SessionEnd closes any remaining trace, explicitly flushes the recorder, stops its per-session timer, and removes it from the registry.
  • Daemon shutdown and idle-timeout cleanup flush already-ended traces from every live recorder.

ArmorClaude also reports cumulative transcript token usage through the separate dashboard token-usage path at Stop. The current observability bridge itself records policy and tool spans; it does not add transcript-derived generation spans to the iap.plan trace.

Enable or opt out

Observability is enabled by default when an API key is available. It uses the configured ArmorIQ backend base URL and sends product armorclaude.

Disable it with the disable_observability plugin option or with:

export ARMORIQ_OBSERVABILITY_DISABLED=true

The plugin option takes precedence over the legacy environment value. Accepted true values include true, 1, yes, y, and on.

Data visibility

Neither integration streams spans to the dashboard while a turn is active.

  • ArmorCodex sends the assembled trace during Stop.
  • ArmorClaude ends the trace during Stop and normally sends it on the next shipper flush.
  • The dashboard polls every 30 seconds.

It is normal for the turn to finish before its trace appears. See the dashboard guide for refresh behavior and troubleshooting for missing or delayed data.

Failure and privacy boundaries

All bridge operations catch telemetry failures so they do not change enforcement. Failed batches can still be dropped, and awaited boundary shipping can still delay hook completion.

Telemetry can include prompts, policy inputs, tool inputs, and tool outputs. The bridges apply bounded sanitization such as depth, item-count, string-length, attribute-size, and known secret-pattern limits. ArmorCodex also redacts a bounded set of sensitive field names before writing observability records. These controls are not general-purpose data-loss prevention and do not guarantee that every secret or personal value is removed.

Avoid placing credentials in prompts or tool arguments, restrict access to the observability dashboard, and use safe placeholders when debugging. See the shared data model for exact span fields and the observability overview for the end-to-end architecture.

On this page