Skip to main content
The oobo CLI syncs commit attribution data (anchors) to a remote server on every commit. By default it points at api.oobo.ai (free accounts at oobo.ai). You can also self-host using dock or your own implementation. This page documents the API that any backend must implement.

Authentication

Authenticated requests include:
Authorization: Bearer <api_key>
Content-Type: application/json
User-Agent: oobo/<version>
API keys are configured via:
oobo auth login --key <your_key>
# or
export OOBO_SECRET_KEY=<your_key>

Endpoint overview

Only ingest is required. The rest are optional.
EndpointMethodAuthRequiredPurpose
/anchors/ingestPOSTBearer tokenYesAccept anchor data from commits
/anchors/verifyGETBearer tokenNoVerify an API key is valid
/anchors/healthGETNoneNoHealth check / connectivity test
/anchors/sharePOSTBearer optionalNoAccept shared sessions

POST /anchors/ingest (required)

Called automatically after every git commit (and other write operations). This is the core endpoint. Behavior: Fire-and-forget from a background thread. The CLI does not block on the response. Timeouts: 2s connect, 10s total. Request body (EventPayload):
{
  "event": "git.commit",
  "timestamp": "2026-03-16T12:00:00Z",
  "oobo_version": "0.1.11",
  "project": {
    "name": "my-app",
    "git_remote": "github.com/user/my-app"
  },
  "anchor": {
    "oobo_version": "0.1.11",
    "commit_hash": "abc123def456...",
    "branch": "main",
    "author": "Dev <dev@co.com>",
    "author_type": "assisted",
    "contributors": [
      { "name": "Dev <dev@co.com>", "role": "human" },
      { "name": "claude", "role": "agent", "model": "claude-sonnet-4" }
    ],
    "committed_at": 1773282899,
    "message": "feat: add auth flow",
    "files_changed": ["src/auth.rs", "src/main.rs"],
    "added": 120,
    "deleted": 45,
    "file_changes": [
      { "path": "src/auth.rs", "added": 100, "deleted": 30, "attribution": "ai", "agent": "claude" },
      { "path": "src/main.rs", "added": 20, "deleted": 15, "attribution": "human" }
    ],
    "ai_added": 100,
    "ai_deleted": 30,
    "human_added": 20,
    "human_deleted": 15,
    "ai_percentage": 78.79,
    "session_ids": ["sess-uuid-123", "sub-uuid-456"],
    "transparency_mode": "on",
    "file_interactions": [
      {
        "path": "src/auth.rs",
        "sessions": [
          { "session_id": "sess-uuid-123", "role": "writer" },
          { "session_id": "sub-uuid-456", "role": "reader" }
        ]
      }
    ],
    "sessions": [
      {
        "session_id": "sess-uuid-123",
        "agent": "claude",
        "model": "claude-sonnet-4",
        "link_type": "explicit",
        "input_tokens": 15000,
        "output_tokens": 8000,
        "cache_read_tokens": 5000,
        "cache_creation_tokens": 2000,
        "duration_secs": 120,
        "tool_calls": 5,
        "files_touched": ["src/auth.rs"],
        "tool_usage": { "Edit": 3, "Bash": 2 },
        "tool_failures": 0,
        "subagent_count": 1,
        "bash_commands": ["cargo test"],
        "thinking_duration_ms": 8500,
        "compact_count": 0,
        "is_subagent": false,
        "is_estimated": false,
        "peer_session_ids": ["sub-uuid-456"]
      },
      {
        "session_id": "sub-uuid-456",
        "agent": "claude",
        "model": "claude-sonnet-4",
        "link_type": "explicit",
        "input_tokens": 3000,
        "output_tokens": 1500,
        "duration_secs": 30,
        "tool_calls": 2,
        "files_touched": ["src/auth.rs"],
        "is_subagent": true,
        "parent_session_id": "sess-uuid-123",
        "subagent_type": "explore",
        "is_estimated": false,
        "peer_session_ids": ["sess-uuid-123"]
      }
    ]
  },
  "transcript": [
    {
      "role": "user",
      "text": "Add auth flow"
    },
    {
      "role": "assistant",
      "text": "I'll create the auth module...",
      "thinking": "The user wants an auth module. I should...",
      "timestamp_ms": 1773282800000
    },
    {
      "role": "assistant",
      "tool_call": {
        "tool_use_id": "tc_001",
        "name": "Edit",
        "input_summary": "Create src/auth.rs with auth flow"
      },
      "timestamp_ms": 1773282810000
    },
    {
      "role": "tool",
      "tool_result": {
        "tool_use_id": "tc_001",
        "name": "Edit",
        "success": true,
        "output_summary": "Created src/auth.rs (100 lines)"
      },
      "timestamp_ms": 1773282811000
    }
  ],
  "session_transcripts": [
    {
      "session_id": "sess-uuid-123",
      "messages": [
        { "role": "user", "text": "Add auth flow" },
        { "role": "assistant", "text": "I'll create the auth module...", "thinking": "...", "timestamp_ms": 1773282800000 }
      ]
    },
    {
      "session_id": "sub-uuid-456",
      "parent_session_id": "sess-uuid-123",
      "subagent_type": "explore",
      "messages": [
        { "role": "user", "text": "Read auth.rs and verify structure" },
        { "role": "assistant", "text": "The auth module looks correct...", "timestamp_ms": 1773282815000 }
      ]
    }
  ]
}

Field reference

Top-level:
FieldTypeDescription
eventstring"git.commit" for commits, "git.push" for pushes
timestampstringISO 8601
oobo_versionstringCLI version
projectobject{ name, git_remote } — git_remote can be null
anchorobject?Full anchor data. Null for non-commit events (e.g. push).
transcriptarrayFlat transcript messages (all sessions concatenated). Empty when transparency is off. Always pre-redacted. See TranscriptMessage below.
session_transcriptsarrayStructured per-session transcripts with parent-child relationships. Empty array when transparency is off or no sessions. See SessionTranscript below. Added in v0.1.11.
TranscriptMessage (items in transcript[] and session_transcripts[].messages[]):
FieldTypeDescription
rolestring"user", "assistant", or "tool"
textstring?Message text content. Null for pure tool-call/tool-result messages.
thinkingstring?Agent’s thinking/reasoning text. Null when not present.
tool_callobject?{ tool_use_id, name, input_summary } — present when the message is a tool invocation.
tool_resultobject?{ tool_use_id, name, success, output_summary? } — present when the message is a tool result.
timestamp_msint?Message timestamp in milliseconds. Null for older payloads.
SessionTranscript (items in session_transcripts[]):
FieldTypeDescription
session_idstringSession this transcript belongs to
parent_session_idstring?Parent session ID if this is a subagent. Null for root sessions.
subagent_typestring?Subagent type (e.g. "explore", "shell", "generalPurpose"). Null for root sessions.
messagesarrayArray of TranscriptMessage for this session only.
anchor object:
FieldTypeDescription
commit_hashstringFull commit SHA
branchstringBranch name
authorstringGit author (Name <email>)
author_typestring"agent", "assisted", "human", or "automated"
contributorsarray[{ name, role, model? }] — role is "human" or "agent"
committed_atintUnix timestamp
messagestringCommit message
files_changedarrayList of changed file paths
added / deletedintTotal lines added/deleted
file_changesarrayPer-file: { path, added, deleted, attribution, agent? }
ai_added / ai_deletedintLines added/deleted by AI
human_added / human_deletedintLines added/deleted by humans
ai_percentagefloat?0.0–100.0
sessionsarrayLinked AI sessions (see below)
transparency_modestring"on" or "off"
file_interactionsarray?Cross-session file interactions. Each entry: { path, sessions: [{ session_id, role }] } where role is "writer", "reader", or "both". Only present when 2+ sessions touched the same files.
anchor.sessions[]:
FieldTypeDescription
session_idstringUnique session ID
agentstringTool name (cursor, claude, gemini, etc.)
modelstring?LLM model identifier
link_typestring"explicit" (hooks) or "inferred" (time-window)
input_tokens / output_tokensint?Token counts
cache_read_tokens / cache_creation_tokensint?Cache token counts
duration_secsint?Session duration
tool_callsint?Number of tool calls
files_touchedarray?Files modified in this session
tool_usageobject?Tool name → call count map, e.g. {"Bash": 12, "Edit": 8}. Null when unavailable.
tool_failuresint?Number of failed tool calls. Null when unavailable.
subagent_countint?Number of subagents spawned during this session. Null when unavailable.
bash_commandsarray?Recent bash commands executed by the agent. Null when unavailable.
thinking_duration_msint?Accumulated thinking/reasoning time in milliseconds. Null when unavailable.
compact_countint?Number of context compaction events. Null when unavailable.
is_subagentboolWhether this is a sub-agent session. Default false.
parent_session_idstring?Parent session ID if this is a subagent. Null for root sessions.
subagent_typestring?Subagent type (e.g. "explore", "shell", "generalPurpose"). Null for root sessions.
peer_session_idsarrayIDs of other sessions this session interacted with via shared files. Empty array when no interactions.
is_estimatedbooltrue = tiktoken estimate, false = native from tool

Responses

StatusMeaningCLI behavior
200 or 202SuccessSilent
409Duplicate (commit_hash + git_remote exists)Silent (treated as success)
401Bad API keyPrints auth error
422Payload rejectedPrints warning, suggests version check
500Server errorPrints warning unless body contains “duplicate”
Success:
{ "success": true, "message": "Anchor ingested" }
Deduplication: Enforce a unique constraint on (git_remote, commit_hash) and return 409 for duplicates. The CLI retries on transient failures.

GET /anchors/verify (optional)

Validates the API key. Called during oobo auth login. Auth: Authorization: Bearer <key> Success (200):
{ "valid": true }
The CLI just checks for a 200 status code. The body is informational. Failure (401):
{ "error": "invalid_token", "message": "API key is invalid" }

GET /anchors/health (optional)

Called by oobo dash to check connectivity. No auth required. Return any 2xx. Body is ignored.
{ "status": "ok" }

POST /anchors/share (optional)

Called by oobo share <session_id>. Accepts a redacted session for sharing. Auth is optional — if a Bearer token is present, the share is associated with the user. If absent, the share is anonymous. Request body:
{
  "session_id": "abc-123-456",
  "source": "cursor",
  "model": "claude-sonnet-4",
  "messages": [
    { "role": "user", "text": "Add auth flow" },
    { "role": "assistant", "text": "I'll create the auth module..." }
  ],
  "stats": {
    "input_tokens": 5000,
    "output_tokens": 12000,
    "duration_secs": 120
  },
  "shared_at": "2026-03-16T12:00:00Z",
  "oobo_version": "0.1.11"
}
model and stats may be null. Messages are always pre-redacted. Response (200 or 201):
{ "url": "https://api.oobo.ai/s/shr_abc123" }
The CLI prints the url to the user.

Self-hosting

By default, the CLI points at api.oobo.ai. To use your own server:
oobo auth set-remote https://oobo.mycompany.com
oobo auth login --key <your_key>
Or in ~/.oobo/config.toml:
[server]
url = "https://oobo.mycompany.com"
api_key = "your_key"
sync = true
For a ready-made self-hosted backend, see dock — a Next.js + Postgres scaffolding you can fork and deploy. Only /anchors/ingest is required. The other endpoints are optional — if not implemented, the corresponding CLI features (auth verify, health check, share upload) will gracefully fail.

What the CLI does NOT send

  • No local filesystem paths
  • No third-party API keys
  • No cost/USD calculations
  • No file contents outside of AI sessions
  • Transcripts are always redacted and only sent when transparency_mode is "on"

Example: curl

curl -X POST https://api.oobo.ai/anchors/ingest \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "git.commit",
    "oobo_version": "0.1.11",
    "project": { "name": "my-app", "git_remote": "github.com/user/my-app" },
    "anchor": {
      "commit_hash": "abc123",
      "branch": "main",
      "author": "Dev <dev@co.com>",
      "author_type": "human",
      "message": "fix: typo",
      "files_changed": ["readme.md"],
      "added": 1, "deleted": 1,
      "file_changes": [{ "path": "readme.md", "added": 1, "deleted": 1, "attribution": "human" }],
      "ai_added": 0, "ai_deleted": 0, "human_added": 1, "human_deleted": 1,
      "ai_percentage": 0.0,
      "sessions": [],
      "transparency_mode": "off"
    }
  }'