Download

name: hd-api description: > Complete reference for Hydrogen Desktop control API endpoints. The control API runs on a dynamic port (find it in the HD log or via port discovery). Use these endpoints to drive HD programmatically: manage the workspace runtime (WSL2 distro or legacy Docker container), control Claude Code, manipulate VS Code, manage workspace tabs, run setup steps, trigger virgin reset, and more. ~200 endpoints. The COMPLETE, always-current catalog is GET /_manifest (every route with a category + description; curated entries are richer). The API Explorer window (Adom menu > API Explorer, or POST /api-explorer/open) renders it with a runnable Try per endpoint. GET /test/help lists the gated /test/* diagnostics. NEVER blind-call destructive endpoints (see the Safety section). Trigger words: HD API, control API, HD endpoint, container exec, claude api, iframe eval, workspace tabs, setup run-all, docker status, reload vscode, browser profiles, open url, virgin reset api, reboot api, diagnostics, ad health, test window-tree, ai cursor, setup panel, screenshot mode.

HD Control API Reference

The control API exposes ~159 endpoints. Don't trust a static count or list in this file to stay exhaustive — the API is self-documenting, with two live sources of truth you should hit first:

Source What it gives you Where
GET /_manifest THE complete catalog — every registered route (path, method, args, category, description, visibility). Curated entries carry richer descriptions; the rest are auto-generated from the router (scripts/gen-manifest.py), so it never drifts. Discover here first — don't guess paths. hd-control/src/lib.rs /_manifest arm
API Explorer window The human face of /_manifest: every endpoint grouped by category tab, with a runnable Try + Copy URL. Open from the Adom menu, or POST /api-explorer/open. Adom menu ▸ API Explorer
GET /test/help The /test/* diagnostics (window enumeration, UIA click, dialog classification, cascade reset). Gated behind HD_TEST_API_ENABLED=1 — 404 by default (expected, not a bug). hd-control/src/lib.rs /test/help arm

⛔ Safety — the API enforces this, and tells you in-band

The API itself now labels, warns about, and gates dangerous endpoints (single source of truth: safety.rs in hd-control). Three tiers:

  • catastrophic — irreversibly destroys real user state: /setup/virgin-reset, /setup/panel/(run-virgin-reset|wipe-keep-auth), /wsl/unregister (deletes the whole workspace distro), /docker/(uninstall|clean-reinstall), /system/reboot (reboots the user's COMPUTER), /config-reset, /claude/wipe-creds, *-delete. Headless calls (no browser Origin header — i.e. YOU) get HTTP 409 confirmation_required and NOTHING happens until you re-call with ?confirm=1. Only add ?confirm=1 when the user explicitly asked for exactly that action — never to make an error go away.
  • destructive — kills/restarts/re-runs real state (recoverable but real): /app/(restart|relaunch-frontend), /reload-vscode, /docker/(stop|restart), /container(-stop|-restart), /workspace/apply-updates, /setup/(run-all|run-step|state| force-close|trigger|countdown), /claude/tabs/close-all, /updater/install. Never call these speculatively or in a sweep.
  • disruptive — the user SEES it (window opens, focus moves, typing lands in their real chat, screen recording starts): /hdbw/open, /api-explorer/open, /open-url, /open-in-profile, /window/foreground, /ai-cursor/*, /recording/native/start, /ui/toast, /claude/(trigger-auth|authorize-in-browser|open-with-message|inject*|send| submit|uia-*|new-chat-and-send|type-and-submit) (the send/submit ones SPEND a real user turn). Run only when the user actually wants that action, and clean up after yourself. (Calling /claude/authorize-in-browser blindly is how you spray OAuth windows into Edge.)

How the API tells you:

  • Before calling: every GET /_manifest entry carries safety + safety_note. Check them before you POST anything unfamiliar. Unlabeled = safe.
  • When you call: responses from these endpoints include a _safety block. READ IT — it says exactly what you just did to the user's machine.
  • The 409 gate is your last chance to stop; it is not an error to route around.

A safe GET/POST returns within a couple seconds with a 2xx or a clean 4xx {ok:false,...}. If you must probe an endpoint you're unsure about, POST an empty {} body and treat a graceful validation error as "it works" — a 500 or a hang is the bug.

Base URL: http://127.0.0.1:<dynamic> — the same loopback address from inside the workspace (WSL2 mirrored networking shares loopback with the Windows host) and on the Windows host. The port is dynamic per launch; read the live URL from ~/.adom/hd-control-url — see "Finding the Control Port" below.

Finding the Control Port

The control API port is dynamic (default 47084, auto-resolves conflicts).

# From HD log
adom-desktop hd_log '{"tail":100}' | grep "control="

# From ports.json
type %APPDATA%\hydrogen-desktop\ports.json

# From inside the WSL2 workspace — USE THIS. HD writes its live control URL to a discovery
# file every launch (mirrored loopback), so you never hunt for the dynamic port:
CTRL="$(cat ~/.adom/hd-control-url)"; curl -sf "$CTRL/health"

# No file access (remote tool, browser)? Probe the DISCOVERY responder: fixed candidate
# ports tried in order (a single fixed port is not bindable on every Windows box; WSL2
# mirrored networking claims ports invisibly). Take the FIRST that answers with
# service == "hydrogen-desktop-discovery"; its control_url is the live control API:
for p in 47080 48080 49180 50280; do
  R=$(curl -sf --max-time 2 "http://127.0.0.1:$p/") && echo "$R" | grep -q hydrogen-desktop-discovery && break
done

From the workspace, read ~/.adom/hd-control-url (e.g. http://127.0.0.1:<control>) — a file, NOT an env var (your non-interactive Bash shells don't source .bashrc/profile.d). HD rewrites it with the live dynamic port every launch. This is the channel for the UI command bus, the live ports map, Claude control, etc. — distinct from adom-cli (ADOM_HYDROGEN_URL → SSE), which is unchanged for web-hydrogen parity.

Calling gotchas (learned the hard way)

  • Never hardcode the port. It's dynamic (47084 is only the default and it auto-resolves conflicts). Read ~/.adom/hd-control-url from the workspace, or GET /ports. Hardcoding 47084 is what makes tools show HD as "down" when it's actually up on another port.
  • Calling from a REMOTE container (hw/cloud)? Use adom-desktop hd_api, the relay's pass-through verb: hd_api {"method":"POST","path":"/...","body":{...}}. It auto-discovers the port; never shell curl through the relay (quoting gets mangled). Long-blocking calls (typed injection) can outlive the relay timeout: the work still completes on HD's side, so verify state instead of re-firing.
  • Getting files OUT (recordings, screenshots): streamed binary, never base64. Endpoints return paths inside the WSL distro; a remote container pulls them with adom-desktop --target <desktop> pull_file {filePaths:["\\\\wsl$\\Adom-Workspace\\<path>"], saveTo:...} (1 MiB binary chunks, sha256-verified, no practical size limit). /recording/native/stop returns the ready-made fileWindows UNC path. Full demo-video pipeline: the hd-demo-recording skill.
  • Confirm it's really the control API, not the SPA. In dev, the frontend file server answers 200 with HTML for any path — so a bare 200 doesn't prove you hit HD's API. Check GET /health returns JSON with service:"hydrogen-desktop-control".
  • Driving HD from a page HD serves? Use relative fetches, not Tauri IPC. Tauri invoke is unavailable on remote http origins; a same-origin fetch('/ports') / fetch('/_manifest') works and sidesteps port discovery entirely.
  • Driving from the CLOUD over the relay: if two Adom Desktops are connected (laptop + a VM), desktop_* verbs need --target <name> (e.g. AdomLapper) or they no-route and look empty; nbrowser_*/hd_* auto-route. PowerShell emits CRLF — strip \r before string-comparing its output. Long calls can exceed the relay's per-call timeout — run detached + poll a marker (see hd-cloud-relay-patterns).

Health & Status

Method Path Description
GET /health HD alive check — returns {ok, port, service}
GET /buildinfo Running build git SHA + build metadata (the only trustworthy "did my build ship?" check)
GET /ports The 4 editable PortConfig ports (proxy, control, cdp, code_server)
GET /ports/all LIVE full port map — every host + WSL2 port HD has bound, with status + mirrored-mode info

AD-health diagnostics (/ad/*)

These verify the adom-desktop / relay / embedded-AD half of HD is alive (the bridge that connects this cloud container to HD). All GET, all return {ok, ...}.

Method Path Description
GET /ad/health Overall embedded-AD health roll-up
GET /ad/process Is the adom-desktop process running
GET /ad/embedded Embedded-AD module status
GET /ad/workspace-direct ⭐ The converged check — direct path to the workspace relay (use this one)
GET /ad/relay Relay (WS/HTTP) reachability
GET /ad/liveness-beacon Liveness beacon freshness
GET /ad/direct-api Direct-API reachability
GET /ad/discovery-file Discovery file present + parseable

Driving HD's UI (command bus)

Open/close/toggle any HD menu, dialog, or panel — the first-class alternative to CDP clicking. See the hd-ui skill.

Method Path Body Description
GET /ui/actions List every drivable UI action {id,label,description,group} (+ _hints)
POST /ui/invoke {"id":"ports.open"} Run a UI action; returns {ok,value} only after the frontend ran it
GET /auth-token HD session token
GET /setup-status Overall setup completion state
GET /diagnostics/all Run all diagnostic checks
GET /diagnostics/{name} Run a single diagnostic check

Workspace / Container Management

These endpoints manage the active workspace runtime (a WSL2 distro by default, or a Docker container under HD_RUNTIME=docker).

Method Path Body Description
GET /container-status Workspace runtime available, image/distro present, workspace exists/running
GET /container-stats CPU, memory, disk usage
POST /container-restart Restart the active workspace runtime (5s timeout)
POST /container-stop Stop the active workspace runtime
POST /container-exec {"command":"..."} Run command in the workspace as user adom
POST /container-delete Remove the workspace
POST /volume-delete Remove project volume (DESTRUCTIVE). Docker runtime only — no volume exists under WSL.
POST /image-delete Remove workspace image
GET /container-images List available workspace images
POST /container-switch-image {"image":"..."} Switch to different image
POST /container-pull-image {"image":"..."} Pull an image
GET /container/logs Workspace stdout/stderr
GET /container/env Workspace environment variables
GET /container/ports Published port mappings
GET /container/disk Disk usage inside the workspace
POST /container/restart Alias for /container-restart

WSL runtime endpoints (default runtime)

When HD runs the workspace as a WSL2 distro, it additionally exposes:

Method Path Body Description
GET /wsl/status WSL distro state (registered, running, code-server reachable)
GET /workspace/health Deep workspace health (distro + code-server + host reachability)
GET /virgin-reset-progress Live progress of an in-flight virgin reset
POST /workspace/terminal-profile Ensure the stable Adom-Workspace Windows Terminal profile (idempotent)
POST /workspace/terminal-profile/remove Remove the Adom-Workspace Windows Terminal profile(s)
POST /workspace/check-updates Check upstream for newer code-server + Claude extension; stage them
POST /workspace/apply-updates Apply staged code-server/extension updates + reload editor

POST /wsl/unregister is REFUSED as deprecated — it was a headless destructive reset (deletes /home/adom/project). Use the Setup panel's Virgin Reset or POST /setup/panel/run-virgin-reset instead.

Docker Desktop (legacy Docker runtime only — HD_RUNTIME=docker)

This /docker/* table applies only when HD is running the legacy Docker runtime.

Method Path Body Description
GET /docker/status Docker Desktop running state
GET /docker/version Docker engine version
GET /docker/containers List all Docker containers
GET /docker/images List all Docker images
POST /docker/start Launch Docker Desktop + start container
POST /docker/stop Stop Docker Desktop
POST /docker/restart Restart Docker Desktop
POST /docker/wait-ready Block until Docker daemon responds
POST /docker/install Install Docker Desktop (downloads ~730 MB)
POST /docker/uninstall Uninstall Docker Desktop
POST /docker/clean-reinstall Uninstall + reinstall Docker Desktop
GET /docker/install-progress Current install step + progress

WebView & VS Code

Method Path Body Description
POST /eval {"js":"..."} JS eval in main HD webview
POST /iframe-eval {"js":"...","contextIndex":N} JS eval in a specific iframe context (0-14)
POST /cdp-eval {"expression":"..."} Direct Chrome DevTools Protocol eval
POST /click {"x":N,"y":N} Click at coordinates in webview
POST /mouse-click {"x":N,"y":N,"button":"left"} Mouse click with options
POST /query {"selector":"..."} Query DOM elements
POST /reload-vscode Reload the VS Code iframe
POST /vscode/hide-activity-items Hide activity-bar items (persisted, overflow-proof)
GET /targets List CDP targets (all webview contexts)
GET /cdp-info CDP connection info (port, URL)
GET /devtools DevTools status

AI cursor (visible cursor for demos / clicking)

Distinct from /click and /mouse-click — this renders a visible AI cursor, glides it, and can click. Use for demos or when the user should see where the AI clicks.

Method Path Body Description
POST /ai-cursor/move {"x":N,"y":N} Move (glide) the visible AI cursor to coordinates
POST /ai-cursor/click {"x":N,"y":N} Move the visible AI cursor there + click
POST /ai-cursor/hide Hide the visible AI cursor

Screenshot primitives

Method Path Body Description
GET /capture/viewport Capture the HD webview viewport (PNG)
POST /shot {...} Screenshot primitive (see hd-self-screenshot for the full workflow)

Port hints

Method Path Body Description
GET /adom-port-hints Read the workspace port-hints map
POST /adom-port-hints {...} Write/merge the workspace port-hints map

Claude Code

Status / auth state (GET)

Method Path Description
GET /claude/status Install state, auth, active session, version
GET /claude/auth-check Deep auth check — file exists, token, expiry
GET /claude/auth-status Auth-status detail
GET /claude/auth-page-state Which auth screen the panel shows: continue-in-browser | subscription-button | chat; returns oauth_url + at_browser_auth_gate
GET /claude/detect-auth-failure Scan webview contexts for auth button/errors
GET /claude/sessions List active Claude sessions

Auth actions (POST)

Method Path Body Description
POST /claude/find-auth-button Find + highlight the sign-in button WITHOUT clicking (validation)
POST /claude/start-auth Find + highlight + CLICK the "Claude.ai Subscription" button to begin OAuth
POST /claude/trigger-auth Alias for start-auth
POST /claude/detect-state Detect current auth/panel state
POST /claude/authorize-in-browser {"timeout_ms":N,"dry_run":B,"fresh":B,"oauth_url":"..."} Auto-click the "Authorize" button on the claude.ai OAuth page (unattended sign-in)
POST /claude/authorize-tab-enter Approve the Adom OAuth tab via Enter (branded picker flow)
POST /claude/inject-creds Push host backup credentials into the workspace
POST /claude/wipe-creds {"backup":B} Wipe Claude credentials (destructive); backup:false keeps the host backup so reinject can restore

Panel / conversation control (POST unless noted)

Method Path Body Description
POST /claude/open Open Claude Code panel (claude-vscode.editor.open)
POST /claude/ensure-open Ensure the panel is open (strict readiness; recovery if not rendered)
POST /claude/open-with-message {"message":"..."} Open + pre-fill message
POST /claude/focus Focus Claude panel
POST /claude/new-conversation Start a fresh conversation (shows sign-in panel when unauthenticated)
POST /claude/new-chat-and-send {"text":"...","typed":B,"submit":B,"delay_ms":N} Open fresh conversation, wait for compose box, type + (default) submit
POST /claude/submit Find + click send button
POST /claude/send {"message":"..."} Send message to Claude
POST /claude/inject {"message":"..."} Inject text into the compose box
POST /claude/inject-message {"message":"..."} Inject into conversation
POST /claude/type-and-submit {"message":"...","delay_ms":N} Type char-by-char (human-paced) + submit
POST /claude/remote-control {"name":"..."} Turn on Claude Code Remote Control (submits /remote-control slash command)
POST /claude/highlight-open-button {"duration":N} Glow animation on Claude button
POST /claude/clear-open-highlight Clear the open-button highlight

OAuth & Auth

Method Path Body Description
POST /claude-auth Full PKCE OAuth flow — opens browser, sets up callback
GET /oauth/callback OAuth callback handler (receives auth code)
POST /auth/proxy varies Proxy auth requests to carbon.adom.inc
POST /open-url {"url":"..."} Open URL (triggers browser picker)
POST /start-oauth-proxy {"port":N} Start OAuth callback proxy on a port
GET /browser-profiles List browsers + profiles with avatars
POST /open-in-profile {"url":"...","browser":"...","profileDir":"..."} Open URL in specific browser profile
POST /browser-picker/opened {...} Frontend reports the browser picker was opened
GET /browser-picker/last-open Last browser-picker open event

Setup Steps & Setup Panel

State / trigger / step control

Method Path Body Description
GET /setup/state Get current setup state (never-run | in-progress | complete)
POST /setup/state {"state":"..."} Set setup state
POST /setup/step/<id> {"continue_after":B} ⭐ Run one step by id (forces panel visible first); continue_after:true runs this step + every step after it
POST /setup/force-close Force-close setup panel
POST /setup/countdown {"seconds":N,"message":"..."} Show countdown overlay
GET /setup/trigger Read trigger file
DELETE /setup/trigger Delete trigger file
POST /config-reset Reset HD config

REFUSED as deprecated (headless runs with no visible UI): POST /setup/run-all, POST /setup/run-step, POST /setup/virgin-reset, POST /wsl/unregister. They return a refusal pointing at the UI-visible path. To run steps: use POST /setup/step/<id> or relaunch HD to auto-run the cascade.

Setup panel control

Method Path Body Description
POST /setup/panel/show Force the setup panel visible
POST /setup/panel/hide Hide the setup panel
POST /setup/panel/show-virgin-reset Show panel + expand the Virgin Reset section
POST /setup/panel/virgin/show Show the Virgin Reset section only
POST /setup/panel/virgin/hide Hide the Virgin Reset section
POST /setup/panel/set-virgin-options {"options":{...}} Set the virgin-reset checkboxes
POST /setup/panel/run-virgin-reset ⭐ The ONLY allowed virgin-reset trigger — UI-visible by construction (shows panel + checkboxes + progress before deleting anything)
POST /setup/panel/wipe-keep-auth Synchronous wipe that KEEPS Adom + Claude tokens
POST /setup/screenshot-mode {"enabled":B} Toggle per-step screenshot capture (default OFF)
GET /setup/screenshot-mode Whether per-step screenshot mode is on

Workspace Tabs

Method Path Body Description
GET /workspace/tabs List all workspace tabs
GET /workspace/tabs/find?name=X Find tab by name (404 if not found)
POST /workspace/tabs {tab spec} Add a new tab

System

Method Path Body Description
GET /system/nonce Get nonce for authenticated reboot
GET /system/reboot-state Check reboot pending state
POST /system/reboot {"nonce":"..."} Reboot Windows (nonce-authenticated)
POST /system/cancel-reboot Cancel pending reboot

App lifecycle & window

Method Path Body Description
POST /app/restart Restart the HD app (kills + respawns the binary)
POST /app/relaunch-frontend Relaunch the HD GUI (kill+respawn) while it is responsive
POST /window/foreground Bring the HD window to the foreground (unminimize + focus)

Diagnostics (/test/*)

Window enumeration, UIA clicking, native-dialog classification, and cascade diagnostics. Gated behind HD_TEST_API_ENABLED=1 (return 404 / "test API disabled" otherwise). GET /test/help self-lists the whole cluster.

Method Path Body Description
GET /test/help Self-lists every /test/* endpoint
GET /test/window-tree Enumerate all visible top-level windows
POST /test/focus {"window":"..."} Focus HD (self) or a window by title substring
POST /test/click-uia {...} Poll UIAutomation for a Button by Name; Invoke when found
GET /test/setup-steps Raw setup-steps.json + derived summary (alias: /test/install-steps)
POST /test/dump-state {...} Write a diagnostic bundle (logs, install-steps, docker, tasklist, window-tree)
POST /test/reset-cascade Delete install-steps.json + emit setup-reset event
POST /test/screenshot-hd-window {...} Capture HD's own window to a PNG file
GET /test/probe-dialogs Enumerate visible windows, classify against the cascade-blocker catalog (UAC, WSL2 update, MSI installer, Docker EULA, …); returns kind + is_blocking per window

Key Patterns

iframe-eval context discovery

Contexts 0-14 cover all webview frames. Context numbers shift when panels open/close. Always scan, never hardcode. The Claude Code extension webview IS reachable through these contexts.

POST /iframe-eval {"js":"document.body.innerText.substring(0,100)","contextIndex":3}

container-exec

Runs as user adom inside the workspace. Use full paths (/home/adom/ not ~/). Tilde doesn't expand through the exec chain.

POST /container-exec {"command":"ls /home/adom/project"}

Re-run a setup step after completion

The trigger file poller STOPS when all steps are done. POST /setup/run-all is REFUSED as deprecated — don't use it. Re-run one step by id (or relaunch HD to auto-run the cascade):

POST /setup/step/<id>