Integration guides
CLI tools
Instrument a Node CLI so that, when a coding agent like Claude Code or Codex runs it, Otis records what the agent was trying to do and how the call went.
@runotis/sdk/cli instruments a Node command-line tool so that when an AI coding agent — Claude Code or Codex — runs it, Otis records:
- What the agent was trying to do — the agent's one-line reason for the call, and the user's broader goal for the session.
- How the call went — its arguments, exit code, duration, and whether it was interrupted.
Calls made in the same agent conversation are grouped together, so you can see how your CLI is actually used by agents, where it fails them, and which tasks drive it.
It is built for the common case where your CLI is launched as a subprocess by a coding agent. The launching agent is detected automatically; when a human runs the same CLI directly, instrumentation is a no-op — nothing is recorded.
Install
npm install @runotis/sdkInitialize Otis once at startup, as in the Node.js guide:
import { initOtis } from "@runotis/sdk";
initOtis({ apiKey: process.env.OTIS_API_KEY, serviceName: "my-cli" });No API key set? Instrumentation is a safe no-op. If OTIS_API_KEY is missing, initOtis disables itself (one startup warning, no crash) and the CLI helpers below do nothing — no file reads, no spans, no network calls. Your CLI behaves exactly as if uninstrumented, so it is safe to add this before the key is provisioned.
Quick start
Wrap your CLI's main work in runInstrumentedCli. It records the run's outcome for you and returns whatever your function returns (or re-throws its error):
import { initOtis } from "@runotis/sdk";
import { runInstrumentedCli } from "@runotis/sdk/cli";
initOtis({ apiKey: process.env.OTIS_API_KEY, serviceName: "my-cli" });
await runInstrumentedCli(
async () => {
// ... your CLI's actual work ...
},
{ name: "my-cli" },
);That is the whole integration. When a coding agent runs your CLI, the call shows up in Otis with the agent's intent attached; when a person runs it directly, nothing is sent.
Manual lifecycle
If you need to control when the call ends — for example to set a specific exit code — use instrumentCli and call end yourself:
import { instrumentCli } from "@runotis/sdk/cli";
const run = instrumentCli({ name: "my-cli" });
try {
const exitCode = await doWork();
await run?.end({ exitCode });
process.exit(exitCode);
} catch (error) {
await run?.end({ error });
process.exit(1);
}instrumentCli returns null when instrumentation is inactive (Otis disabled, not launched by a recognised agent, or the user opted out of telemetry), so the run?. calls are no-ops in those cases. end flushes telemetry before it resolves, so await it before the process exits.
What gets recorded
When the CLI is run by a coding agent, each invocation records:
| Recorded | From |
|---|---|
| The agent's reason for this call | The agent's own account of the command — its description of the call (Claude Code) or its reasoning just before running it (Codex) |
| The user's broader session goal | The prompt that started the conversation |
| The command's arguments | process.argv (set captureInput: false to omit) |
| Exit code and duration | The run's outcome |
| Whether the run was interrupted | An aborted call (e.g. the user stopped the agent) |
The agent's reason and session goal are read from the launching agent's local session history on the machine running the CLI — Claude Code's session transcript or Codex's session rollout. If that history isn't available, or its format isn't recognised, the call is still recorded with everything else — only the intent is omitted.
Supported agents. Claude Code (detected via CLAUDECODE) and Codex (detected via CODEX_THREAD_ID). The recorded span is identical whichever launched the CLI — only the cli.host_agent value (claude-code / codex) differs — so both flow through the same analytics.
Privacy and consent
The CLI integration is designed to be safe to ship in a tool other people run:
- Off-agent runs are never recorded. Without a recognised agent parent, every helper is a no-op. (Pass
always: trueif you want to record direct runs too.) - The host's telemetry opt-out is respected. The cross-vendor
DO_NOT_TRACKandDISABLE_TELEMETRYenvironment variables suppress instrumentation under any agent. Each agent's own opt-out is also honoured for its runs: Claude Code'sCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC(environment or Claude Code settings), and Codex's[analytics] enabled = falsein~/.codex/config.toml($CODEX_HOME/config.toml) — the same flag that disables analytics across Codex's own surfaces. - PII is redacted. Captured arguments and recovered intent run through the SDK's client-side PII redaction before anything is sent. See Privacy. Set
redact: falseto opt out, orcaptureInput: falseto omit arguments entirely. - It never changes your CLI's behaviour. Reading session history and sending telemetry are best-effort and fully isolated; a failure there never throws into your CLI or alters its exit code.
Options
Both instrumentCli and runInstrumentedCli accept the same options:
| Option | Default | Description |
|---|---|---|
name | script basename | Name for this CLI in Otis. |
surface | the CLI name | Named origin stamped on the span (otis.surface → Surface column). Defaults to name, so every CLI flow is labelled without extra wiring; set it to group several CLIs under one label. Because name is always resolved, this takes precedence over any initOtis({ surface }) default for CLI spans — set it here, not just at init. See Tracing → Surface. |
intent | true | Recover the agent's intent from its session history. Set false to record only arguments and outcome. |
captureInput | true | Record the command's arguments. Set false for sensitive arguments. |
redact | true | Run captured arguments and intent through PII redaction. |
always | false | Instrument even when not launched by a recognised agent. |
otis | singleton | A specific Otis instance, instead of the one from initOtis. |
Detecting the agent yourself
If you want to branch your own logic on whether a coding agent launched the process, detectHost() returns the normalised host (with hostAgent set to claude-code or codex) when one did, and null otherwise:
import { detectHost } from "@runotis/sdk/cli";
const host = detectHost();
if (host) {
// running under a coding agent — e.g. emit machine-readable output
console.error(`launched by ${host.detection.hostAgent}`);
}The per-agent detectors detectClaudeCode() and detectCodex() are also exported if you need to branch on a specific agent.