OtisDocs

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/sdk

Initialize 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:

RecordedFrom
The agent's reason for this callThe 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 goalThe prompt that started the conversation
The command's argumentsprocess.argv (set captureInput: false to omit)
Exit code and durationThe run's outcome
Whether the run was interruptedAn 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.

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: true if you want to record direct runs too.)
  • The host's telemetry opt-out is respected. The cross-vendor DO_NOT_TRACK and DISABLE_TELEMETRY environment variables suppress instrumentation under any agent. Each agent's own opt-out is also honoured for its runs: Claude Code's CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC (environment or Claude Code settings), and Codex's [analytics] enabled = false in ~/.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: false to opt out, or captureInput: false to 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:

OptionDefaultDescription
namescript basenameName for this CLI in Otis.
surfacethe CLI nameNamed origin stamped on the span (otis.surfaceSurface 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.
intenttrueRecover the agent's intent from its session history. Set false to record only arguments and outcome.
captureInputtrueRecord the command's arguments. Set false for sensitive arguments.
redacttrueRun captured arguments and intent through PII redaction.
alwaysfalseInstrument even when not launched by a recognised agent.
otissingletonA 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.

On this page