Any agent or custom harness
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.
1. Install the binary and daemon
Section titled “1. Install the binary and daemon”Install the compiled program from downloads and releases, then start the daemon:
handup service install # user service; or: handup serve --foregroundhandup doctor --json # daemon reachable, versions matchhandup 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.
2. Pick a transport
Section titled “2. Pick a transport”| 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.
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"}]}'3. Build the request
Section titled “3. Build the request”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).
4. Persist the id, then wait
Section titled “4. Persist the id, then wait”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 untilpendingis empty;list_requests {"session":…, "status":"pending"}recovers ids after context loss. - CLI:
handup wait ID --json(timeout disabled), orhandup status ID --json. - HTTP:
GET /v1/requests/{id}/wait?timeout=60suntilstatusis notpending. Local and paired-device clients can instead watch theGET /v1/eventsWebSocket.
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.
5. Gate execution on the decision
Section titled “5. Gate execution on the decision”| 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}plusfields(approvals),answers(questions), orrun_result(desktop execution). - CLI
ask --wait --json/wait --json: the decision object (option,feedback,fields,content_hash,decided_by, …) plusidandstatus; question answers are infields.answers. - HTTP and other CLI
--jsoncommands: the full request, decision underdecision.
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.
6. Clean up
Section titled “6. Clean up”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.
7. Tell the model the policy
Section titled “7. Tell the model the policy”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:
handup prompt --agent generic >> AGENTS.mdThe agent skill bundles the same contract with offline docs for skill-aware agents. A skill alone does not install handup or intercept tools.
Checklist
Section titled “Checklist”-
handup doctor --jsonpasses 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/answeredpermits action;run_resultprevents duplicate execution; edits fromdecision.fieldsapplied - 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