Getting Started
This guide takes you from zero to seeing your AI coding agent telemetry in the dashboard. It covers installing the hook, wiring up any of the seven supported agents, importing existing session history, and verifying your data appears.
Prerequisite: an observability server must be running. If you are setting up the platform itself, see the root README for local development and deployment docs for production.
Supported agents
The aiot hook binary captures events from seven AI coding agents.
You can install it for one or more — each agent is wired independently.
| Agent | --agent flag |
Config location | Transcripts |
|---|---|---|---|
| Claude Code | claude-code (default) |
~/.claude/settings.json |
yes |
| OpenAI Codex CLI | codex |
~/.codex/hooks.json or ~/.codex/config.toml |
yes |
| Gemini CLI | gemini-cli |
~/.gemini/settings.json |
yes |
| GitHub Copilot CLI | copilot |
~/.copilot/hooks/aiot.json |
no |
| Pi | pi |
~/.pi/agent/extensions/telemetry.ts |
yes |
| omp (oh-my-pi) | omp |
~/.omp/agent/hooks/telemetry.ts |
yes |
| opencode | opencode |
~/.config/opencode/plugin/telemetry.ts |
yes |
1. Install the hook binary
Option A — Installer script (recommended)
curl -fsSL https://raw.githubusercontent.com/yorch/ai-agents-observability/main/scripts/install.sh | bashUse bash -s -- --version v1.2.0 to pin a release or
bash -s -- --prefix "$HOME/.local/bin" to install without sudo:
curl -fsSL https://raw.githubusercontent.com/yorch/ai-agents-observability/main/scripts/install.sh | bash -s -- --prefix "$HOME/.local/bin"Option B — Manual download
Download the binary for your platform from the GitHub releases page:
| Platform | Binary name |
|---|---|
| macOS (Apple Silicon) | aiot-darwin-arm64 |
| macOS (Intel) | aiot-darwin-x64 |
| Linux (ARM64) | aiot-linux-arm64 |
| Linux (x86-64) | aiot-linux-x64 |
Then make it executable and move it to your PATH:
chmod +x aiot-<os>-<arch>mkdir -p ~/.local/binmv aiot-<os>-<arch> ~/.local/bin/aiotFor checksum verification and air-gapped installation, see docs/deploy/hook-binary.md.
2. Configure the server endpoints
If the observability server is running on localhost (the default during local development), you can skip this step. For a remote deployment:
aiot config set web-url https://observability.example.comaiot config set ingest-url https://ingest.example.comVerify with:
aiot config show3. Authenticate
Link the hook to your account so telemetry is routed to your dashboard:
aiot loginThis prints a URL and a short device code. Open the URL in your browser, enter
the code, and authorize. Your auth token is stored locally in
~/.aiot/identity.json.
4. Install hooks for your agent
The install command writes background service files (launchd on macOS,
systemd on Linux) for the flusher and shipper daemons, and auto-detects and
auto-wires any supported agent harnesses it finds on your machine. Snippets
are printed only for agents it could not detect, so you can wire them by
hand.
# Default: Claude Codeaiot install
# For any other agentaiot install --agent codexaiot install --agent gemini-cliaiot install --agent copilotaiot install --agent piaiot install --agent ompaiot install --agent opencodeIf install could not auto-wire an agent, copy the printed snippet into the
file indicated by the output. The exact snippet for each agent is also
documented below as a fallback reference.
The snippets below are generated by
aiot install --agent <name>. If something does not match, re-runinstall— the binary is the source of truth and the snippets may have been updated since this doc was written.
Claude Code
Add to ~/.claude/settings.json (merge with any existing hooks object):
{ "hooks": { "Notification": [ { "hooks": [{ "args": ["hook", "notification"], "command": "~/.local/bin/aiot", "type": "command" }] } ], "PostToolUse": [ { "hooks": [{ "args": ["hook", "post-tool-use"], "command": "~/.local/bin/aiot", "type": "command" }] } ], "PreCompact": [ { "hooks": [{ "args": ["hook", "pre-compact"], "command": "~/.local/bin/aiot", "type": "command" }] } ], "PreToolUse": [ { "hooks": [{ "args": ["hook", "pre-tool-use"], "command": "~/.local/bin/aiot", "type": "command" }] } ], "SessionStart": [ { "hooks": [{ "args": ["hook", "session-start"], "command": "~/.local/bin/aiot", "type": "command" }] } ], "Stop": [ { "hooks": [{ "args": ["hook", "stop"], "command": "~/.local/bin/aiot", "type": "command" }] } ], "SubagentStop": [ { "hooks": [{ "args": ["hook", "subagent-stop"], "command": "~/.local/bin/aiot", "type": "command" }] } ], "UserPromptSubmit": [ { "hooks": [{ "args": ["hook", "user-prompt-submit"], "command": "~/.local/bin/aiot", "type": "command" }] } ] }}Replace ~/.local/bin/aiot with the actual path to your binary
if you installed it elsewhere.
Codex CLI
Codex has two wiring modes. The install command detects which one to use.
Default (notify wrapper): Save this as ~/.codex/aiot-notify.sh
and chmod +x it:
#!/bin/shprintf '%s' "$1" | ~/.local/bin/aiot hook turn-complete --agent codexThen point Codex at the wrapper in ~/.codex/config.toml:
notify = ["/home/user/.codex/aiot-notify.sh"]Richer capture (lifecycle hooks): If you enable experimental hooks in
~/.codex/config.toml:
[features]hooks = truethen re-run aiot install --agent codex and write the output to
~/.codex/hooks.json. This captures per-tool events in addition to turn-level
data. Not available on Windows.
Gemini CLI
Add to ~/.gemini/settings.json (or .gemini/settings.json in a project):
{ "hooks": { "AfterAgent": [ { "hooks": [{ "command": "\"~/.local/bin/aiot\" hook after-agent --agent gemini-cli", "name": "aiot-after-agent", "timeout": 5000, "type": "command" }] } ], "AfterModel": [ { "hooks": [{ "command": "\"~/.local/bin/aiot\" hook after-model --agent gemini-cli", "name": "aiot-after-model", "timeout": 5000, "type": "command" }] } ], "AfterTool": [ { "hooks": [{ "command": "\"~/.local/bin/aiot\" hook after-tool --agent gemini-cli", "name": "aiot-after-tool", "timeout": 5000, "type": "command" }] } ], "BeforeAgent": [ { "hooks": [{ "command": "\"~/.local/bin/aiot\" hook before-agent --agent gemini-cli", "name": "aiot-before-agent", "timeout": 5000, "type": "command" }] } ], "BeforeTool": [ { "hooks": [{ "command": "\"~/.local/bin/aiot\" hook before-tool --agent gemini-cli", "name": "aiot-before-tool", "timeout": 5000, "type": "command" }] } ], "Notification": [ { "hooks": [{ "command": "\"~/.local/bin/aiot\" hook notification --agent gemini-cli", "name": "aiot-notification", "timeout": 5000, "type": "command" }] } ], "PreCompress": [ { "hooks": [{ "command": "\"~/.local/bin/aiot\" hook pre-compress --agent gemini-cli", "name": "aiot-pre-compress", "timeout": 5000, "type": "command" }] } ], "SessionEnd": [ { "hooks": [{ "command": "\"~/.local/bin/aiot\" hook session-end --agent gemini-cli", "name": "aiot-session-end", "timeout": 5000, "type": "command" }] } ], "SessionStart": [ { "hooks": [{ "command": "\"~/.local/bin/aiot\" hook session-start --agent gemini-cli", "name": "aiot-session-start", "timeout": 5000, "type": "command" }] } ] }}GitHub Copilot CLI
Write to ~/.copilot/hooks/aiot.json:
{ "disableAllHooks": false, "hooks": { "agentStop": [{ "command": ["~/.local/bin/aiot", "hook", "agent-stop", "--agent", "copilot"], "timeoutSec": 5, "type": "command" }], "notification": [{ "command": ["~/.local/bin/aiot", "hook", "notification", "--agent", "copilot"], "timeoutSec": 5, "type": "command" }], "postToolUse": [{ "command": ["~/.local/bin/aiot", "hook", "post-tool-use", "--agent", "copilot"], "timeoutSec": 5, "type": "command" }], "postToolUseFailure": [{ "command": ["~/.local/bin/aiot", "hook", "post-tool-use-failure", "--agent", "copilot"], "timeoutSec": 5, "type": "command" }], "preCompact": [{ "command": ["~/.local/bin/aiot", "hook", "pre-compact", "--agent", "copilot"], "timeoutSec": 5, "type": "command" }], "preToolUse": [{ "command": ["~/.local/bin/aiot", "hook", "pre-tool-use", "--agent", "copilot"], "timeoutSec": 5, "type": "command" }], "sessionEnd": [{ "command": ["~/.local/bin/aiot", "hook", "session-end", "--agent", "copilot"], "timeoutSec": 5, "type": "command" }], "sessionStart": [{ "command": ["~/.local/bin/aiot", "hook", "session-start", "--agent", "copilot"], "timeoutSec": 5, "type": "command" }], "subagentStop": [{ "command": ["~/.local/bin/aiot", "hook", "subagent-stop", "--agent", "copilot"], "timeoutSec": 5, "type": "command" }], "userPromptSubmitted": [{ "command": ["~/.local/bin/aiot", "hook", "user-prompt-submitted", "--agent", "copilot"], "timeoutSec": 5, "type": "command" }] }, "version": 1}Copilot CLI does not expose transcript paths, so transcript archiving is not available for this agent.
Pi
Create ~/.pi/agent/extensions/telemetry.ts (or .pi/extensions/telemetry.ts
in a project):
// ~/.pi/agent/extensions/telemetry.ts (or .pi/extensions/telemetry.ts)import { spawn } from "node:child_process";
const KINDS: Record<string, string> = { before_agent_start: 'user-prompt-submit', session_before_compact: 'pre-compact', session_shutdown: 'session-end', session_start: 'session-start', tool_call: 'pre-tool-use', tool_result: 'post-tool-use', turn_end: 'stop',};
export default function (pi: any) { for (const [native, kind] of Object.entries(KINDS)) { pi.on(native, async (event: any, ctx: any) => { try { const payload = { ...event, cwd: ctx?.cwd ?? process.cwd(), sessionId: ctx?.sessionManager?.sessionId ?? event?.sessionId, sessionFile: ctx?.sessionManager?.path ?? undefined, }; const p = spawn("~/.local/bin/aiot", ['hook', kind, '--agent', 'pi'], { stdio: ['pipe', 'ignore', 'ignore'], detached: true, }); p.stdin.end(JSON.stringify(payload)); p.unref(); } catch { // Telemetry must never break the agent: swallow and continue. } // Observe only: this handler never blocks a tool call and never // rewrites a result, even though the API allows both. }); }}omp (oh-my-pi)
Create ~/.omp/agent/hooks/telemetry.ts (or ~/.oh-omp/agent/hooks/, or
.omp/hooks/ in a project):
// ~/.omp/agent/hooks/telemetry.ts (or ~/.oh-omp/agent/hooks/, or .omp/hooks/)import { spawn } from "node:child_process";
const KINDS: Record<string, string> = { before_agent_start: 'user-prompt-submit', session_before_compact: 'pre-compact', session_shutdown: 'session-end', session_start: 'session-start', tool_call: 'pre-tool-use', tool_result: 'post-tool-use', turn_end: 'stop',};
export default function (omp: any) { for (const [native, kind] of Object.entries(KINDS)) { omp.on(native, async (event: any, ctx: any) => { try { const payload = { ...event, cwd: ctx?.cwd ?? process.cwd(), sessionId: ctx?.session?.id ?? event?.sessionId, sessionFile: ctx?.session?.path ?? undefined, }; const p = spawn("~/.local/bin/aiot", ['hook', kind, '--agent', 'omp'], { stdio: ['pipe', 'ignore', 'ignore'], detached: true, }); p.stdin.end(JSON.stringify(payload)); p.unref(); } catch { // Telemetry must never break the agent: swallow and continue. } // Observe only: this handler never blocks a tool call and never // rewrites a result, even though the API allows both. }); }}
// Alternative, if you already run the third-party `omp-hooks` plugin: it makes// OMP execute settings.json command hooks, so you can wire// "~/.local/bin/aiot hook <kind> --agent omp" there instead. We ship the native module// because it needs no third-party package to keep working.opencode
Create ~/.config/opencode/plugin/telemetry.ts:
import type { Plugin } from '@opencode-ai/plugin';export const telemetry: Plugin = async () => ({ event: async ({ event }) => { const map = { 'session.created': 'session-start', 'tool.execute.before': 'pre-tool-use', 'tool.execute.after': 'post-tool-use', 'session.idle': 'session-idle', }; const kind = map[event.type]; if (!kind) return; const p = Bun.spawn(['~/.local/bin/aiot', 'hook', kind, '--agent', 'opencode'], { stdin: 'pipe' }); p.stdin.write(JSON.stringify(event.properties ?? {})); await p.stdin.end(); },});5. Verify
Check that everything is healthy:
aiot statusThis shows your login state, queue depth, and whether the flusher and shipper services are running.
Then start a session in your agent. After it ends, refresh your My Agents page — the session should appear within a few seconds.
6. Import existing session data
If you have historical sessions from before the hook was installed, you can backfill them. Imported events use deterministic IDs and the server deduplicates them, so imports are safe to re-run.
Supported agents for import
| Agent | --agent flag |
Historical source |
|---|---|---|
| Claude Code | claude-code (default) |
~/.claude/projects/**/*.jsonl |
| Codex CLI | codex |
~/.codex/sessions/**/rollout-*.jsonl |
| opencode | opencode |
~/.local/share/opencode/opencode.db |
| Pi | pi |
~/.pi/agent/sessions/**/*.jsonl |
| omp | omp |
~/.omp/agent/sessions/**/*.jsonl (also probes ~/.oh-omp) |
Gemini CLI and Copilot CLI do not have import sources. Their session data is not stored in a scannable local format that the hook can read. Live capture works for both; only historical backfill is unavailable.
Import commands
# Preview what would be imported without sending anythingaiot import --dry-run
# Import all history from the default agent (Claude Code)aiot import
# Import from a specific dateaiot import --since 2026-01-01
# Import from another agentaiot import --agent codex --dry-runaiot import --agent opencode --since 2026-01-01aiot import --agent piaiot import --agent omp
# Import a single session (events only, no transcripts)aiot import --agent codex --session <session-id> --no-transcriptsImport flags
| Flag | Description |
|---|---|
--agent <name> |
Select claude-code, codex, opencode, pi, or omp |
--since YYYY-MM-DD |
Skip events older than this date |
--session <id> |
Import only one native or normalized session ID |
--no-transcripts |
Skip transcript uploads |
--dry-run |
Parse and count without posting anything |
--quiet |
Suppress per-session progress output |
Authentication (aiot login) is required unless --dry-run is
passed. Transcripts pass through the same client-side redaction and compression
as live uploads.
Managing telemetry
| Command | Description |
|---|---|
aiot status |
Show auth status, queue depth, service state |
aiot pause |
Temporarily stop sending telemetry |
aiot resume |
Re-enable telemetry |
aiot uninstall |
Remove hooks and background services |
aiot purge-local |
Delete all local data (queue, logs, identity) |
You can also manage privacy settings from the Privacy page in the dashboard.
What to look at next
Once data is flowing, explore the dashboard:
- My Agents — your sessions, costs, and trends
- Sessions — searchable session list with transcript viewer
- Insights — effectiveness signals and friction patterns
- Trends — your activity over time
- PRs — sessions correlated to your GitHub pull requests
For the full CLI reference, see apps/hook/README.md. For deployment details, see docs/deploy/.