Skip to content

Any agent or custom harness

View as Markdown

Use this guide to add handup to an agent or harness without a dedicated adapter (your own agent loop, an SDK app, a framework, a CI job). Named clients have shorter guides: Claude Code, Codex, Cursor, omp.

Your harness asks and waits for the human. Ordinary approval permits it to run the reviewed action; desktop Run returns run_result instead, which the harness must consume without executing the command again.

Install the compiled program from downloads and releases, then start the daemon:

Terminal window
handup service install # user service; or: handup serve --foreground
handup doctor --json # daemon reachable, versions match

handup ask starts the daemon on demand unless daemon.autostart is false; wait, status and MCP do not. A harness running in a container or on another host needs the HTTP route in step 2.

Harness can Use Wait mechanism
Speak MCP (stdio) handup mcp request_approval / ask_question with wait, or wait:false + wait_requests
Run shell commands handup CLI ask --wait --json, wait ID --json, exit codes
Only make HTTP calls Daemon API (local) or remote listener with a submit token GET /v1/requests/{id}/wait?timeout=60s loop
Intercept its own tool calls Any of the above from your permission hook Map the decision to allow/deny; see the Claude hook as a model

Prefer MCP when the model itself decides what needs approval; prefer a hook in the harness when every call to a tool must be gated regardless of the model.

MCP. Register a stdio server whose command is handup mcp, started in the project directory. handup integrate mcp --client claude-code|codex|cursor|omp writes the entry for known clients; for others add the equivalent of:

{"mcpServers":{"handup":{"command":"handup","args":["mcp"]}}}

Tools: request_approval, ask_question, check_request, wait_requests, cancel_request, list_requests. Arguments and response shapes: MCP.

CLI. handup ask --request - --wait --json < request.json takes full request JSON (handup schema request); flags cover common cases (--title, --command, --git-diff, --preview TYPE:PATH). Queue commands emit JSON only with --json, whatever output_format says. See Shell and CI.

HTTP. Locally, the API listens on the Unix socket ($XDG_RUNTIME_DIR/handup/handup.sock, no auth beyond file permissions) and on daemon.listen (127.0.0.1:7465, bearer token from $XDG_STATE_HOME/handup/token, loopback Host only). Remote harnesses use a named submit token, which can create and read only its own requests. Routes: OpenAPI.

Terminal window
curl --unix-socket "$XDG_RUNTIME_DIR/handup/handup.sock" \
-H 'Content-Type: application/json' http://handup/v1/requests \
-d '{"title":"Deploy staging","kind":"command","previews":[{"type":"command","content":"./deploy staging"}]}'

Required: title. Describe the exact action, its scope, risk and rollback in title/summary, and snapshot the evidence the human needs as previews (command, diff, file, JSON, image, email, …). Strip secrets first: previews are stored and shown on every paired device.

Field Use
kind, risk command, edit, review, question, custom; low/medium/high drives notification urgency and rules
source.agent, source.session, source.session_title, source.cwd Who is asking; self-declared, shown to the human and matched by rules
input Editable object; the human’s edits come back in decision.fields
options Custom choices {id,label,outcome} (1–32); omit for Approve/Deny
timeout, on_timeout Default is no deadline; a duration opts into expiry (deny by default)
dedupe_key Identical pending requests with the same key collapse into one id
callback_url Daemon POSTs the decision there (callbacks)

Questions instead of approvals: MCP ask_question, or kind: "question" with input.questions (see the MCP guide).

Save the returned id before waiting, keyed to the action it gates. Tool and shell timeouts kill waiters long before a human decides (omp bash: 300s, MCP clients: 30–60s); a killed waiter is not an answer and the request stays pending. Reattach with the same id:

  • MCP: wait_requests {"ids":[…]} in a loop until pending is empty; list_requests {"session":…, "status":"pending"} recovers ids after context loss.
  • CLI: handup wait ID --json (timeout disabled), or handup status ID --json.
  • HTTP: GET /v1/requests/{id}/wait?timeout=60s until status is not pending. Local and paired-device clients can instead watch the GET /v1/events WebSocket.

Never create a second request because a wait timed out. Keep the agent turn alive while answers you need are pending: an idle agent is not woken by them.

Status Meaning Harness action
approved Human approved If run_result is present, consume it and do not run again; otherwise run exactly the reviewed action with decision.fields edits applied
answered Question submitted Use answers (MCP) or fields.answers (CLI/HTTP decision)
denied Human said no Stop; show decision.feedback; revise only if feedback asks
expired, cancelled No decision Stop; not permission
pending Undecided Keep waiting; not permission
Any error Transport/validation failure Stop; fail closed

Bind execution to content_hash: it covers the title, summary, kind, previews, options, input and callback URL the human saw. If the action you are about to run differs from the reviewed one, submit a new request instead of reusing the approval. A denial is a normal answer (MCP isError=false, CLI exit 1); never resubmit it unchanged.

Response shapes differ by transport:

  • MCP: {id,status,option,feedback,content_hash} plus fields (approvals), answers (questions), or run_result (desktop execution).
  • CLI ask --wait --json / wait --json: the decision object (option, feedback, fields, content_hash, decided_by, …) plus id and status; question answers are in fields.answers.
  • HTTP and other CLI --json commands: the full request, decision under decision.

CLI ask --wait, wait, status and show return 0 approved/answered, 1 denied, 2 expired, 3 cancelled, 4 error, or 5 ran in the desktop app. Nonblocking ask, status and show also exit 0 while pending, so read status there.

Exit 5 means run_result is present, not that the command succeeded. It can contain a nonzero exit_code or error while status remains approved. Read the result fields and stop duplicate execution even on failure. Permission hooks should block the intercepted call with the run summary (including human feedback) rather than returning allow when a result is present.

Retract requests your harness no longer needs (plan changed, task aborted) with MCP cancel_request, handup cancel ID, or POST /v1/requests/{id}/cancel. Never cancel to dodge a pending decision you dislike.

Add the approval contract to the agent’s instructions so it asks before destructive, published, costly, credential-related or ambiguous actions and handles denials correctly:

Terminal window
handup prompt --agent generic >> AGENTS.md

The agent skill bundles the same contract with offline docs for skill-aware agents. A skill alone does not install handup or intercept tools.

  • handup doctor --json passes where the harness runs
  • Transport wired: MCP server entry, CLI on PATH, or HTTP endpoint + credential
  • Request ids persisted before waiting; reattach path tested by killing a waiter
  • Only approved/answered permits action; run_result prevents duplicate execution; edits from decision.fields applied
  • Denial feedback reaches the model; no automatic retry
  • Errors, timeouts and unknown statuses fail closed
  • Approval policy present in the agent’s instructions
  • End-to-end test: approve once, deny once, and answer a question from handup inbox