Integration interface

Onboard a harness

A harness is any CLI coding agent — Claude Code, Codex, Copilot, or your own. RubberDuckHQ talks to all of them through one adapter contract. Onboarding a new agent is: implement that contract once, add one line to the registry. The core never imports a specific agent — it loads whichever the session declares.

Two ways a session runs

Every session is one of two kinds — the same watched / launched labels you see on the dashboard. A harness can support one or both; supporting more unlocks more.

The contract

A harness is a class implementing this interface (Python today; the shape is what matters). Each method is small and single-purpose:

class Harness:
    name: str                       # registry key, e.g. "codex"
    hook_spec: HookSpec | None      # for WATCHED support; None = launch-only

    def __init__(self, command: str): ...

    # ── needed to LAUNCH the agent ──
    def launch_command(self, *, cwd, session_key, initial_prompt) -> list[str]
        # the argv to start the agent

    def restore_command(self, *, cwd, session_key) -> list[str]
        # the argv to resume an existing session

    def detect_state(self, recent_output: str) -> SessionState
        # idle | busy | waiting | terminated, read from terminal output
        # (used when there are no hook events driving state)

    def tool_in(self, recent_output: str) -> str | None
        # which tool is running, if detectable from output

    # ── richer history (optional) ──
    def locate_transcript(self, *, cwd, session_id) -> Path | None
    def read_transcript(self, *, cwd, session_id) -> list[{role, text}]
        # the conversation as uniform {role, text} records, newest-last
        # (each agent reads its own native format: JSONL, SQLite, …)

To support WATCHED: HookSpec

To let people watch a session they started themselves, the agent must have a hook system you declare with a HookSpec: the config file location (global + repo-local) and two pure functions that build (merge our hook entries in) and strip (remove them) on the parsed config — so install and uninstall stay symmetric and idempotent.

hook_spec = HookSpec(
    global_rel = Path(".codex") / "hooks.json",   # ~/.codex/hooks.json
    repo_rel   = Path(".codex") / "hooks.json",   # <repo>/.codex/hooks.json
    build = claude_style_build,   # add our entries to the agent's config
    strip = claude_style_strip,   # remove them again
)

Agents whose config is shaped like Claude's reuse the shared build/strip helpers — Codex does exactly this. A differently-shaped config (e.g. Copilot's) supplies its own pair.

The event contract

The installed hook posts the agent's lifecycle events to POST /events as one normalized JSON shape, whatever the agent's native field names. RubberDuckHQ accepts snake_case or camelCase and maps to:

{
  "event_type":  "SessionStart | UserPromptSubmit | PreToolUse |
                  PostToolUse | PermissionRequest | Notification |
                  Stop | SessionEnd",
  "session_id":  "<agent's own session id>",
  "session_key": "<set by Rubberduck on launch, else null>",
  "cwd":         "/path/the/agent/runs/in",
  "tool_name":   "Bash | Edit | WebFetch | …",
  "tool_input":  { … },          # the command, url, file, etc.
  "prompt":      "<user prompt text>",
  "runtime":     "claude-code | codex | copilot",
  "agent_pid":   12345           # so a watched session's death is detected
}

From these, the dashboard derives session state, the Pulse feed, the Needs-human panel, and durable history — uniformly across agents.

Register it

One entry wires the agent into everything: the --agent choices, the New-session picker, runtime construction, hook install, and transcript reading for checkpoints.

REGISTRY = {
    "claude-code": ClaudeCodeRuntime,
    "codex":       CodexRuntime,
    "copilot":     CopilotRuntime,
    "your-agent":  YourRuntime,   # <- onboard here
    "generic":     GenericRuntime,
}

Levels of support (degrade gracefully)

A harness gets exactly the support its capabilities allow — nothing is all-or-nothing:

CapabilityWhat it unlocks
Launch only (the contract)Start & resume the agent from the dashboard; coarse state from its output. Works for any CLI (the generic runtime).
+ HookSpec → WatchedAlso watch a session you started yourself; precise state, the Pulse feed, Needs-human, durable history.
+ Transcript readerHigh-quality checkpoints & handoff summaries from the agent's own messages.
+ Blocking approval hookApprove / Deny permission prompts straight from the dashboard. (Claude Code, Copilot.)

Approvals, specifically

If the agent has a pre-exec hook that can block and return a decision, its permission prompts route to the dashboard: the hook registers the request (POST /approvals), long-polls GET /approvals/:id/decision, and returns the agent's allow/deny once you click — no keystroke faking, fail-open if RubberDuckHQ is down. Agents without such a hook (Codex) still surface the request and offer a one-click jump to their terminal to answer there.

← Back to RubberDuckHQ