Connect Claude Code (or any Claude agent) to Futex so it can ask a human for
permission before doing something — durable, policy-driven approvals — via built-in MCP tools or
the HTTP API. Base URL: https://futex.rodmena.co.uk.
Open Claude Code and say:
Read https://futex.rodmena.co.uk/claude and set me up to use Futex.
Claude will read this page and follow the steps below.
Futex is a human-in-the-loop control plane. Your agent doesn't hard-code who approves what —
it requests a decision against a policy, gets a durable decision_id back
immediately (non-blocking), and a human reviewer approves/rejects via a secure emailed link.
Your agent learns the outcome by polling or via a webhook. The result is a normative
outcome envelope: approved, rejected, changes_requested, timed_out, or cancelled, with reasons.
You need a Futex API key — a Bearer token that looks like futex_sk_.... It is scoped
to a tenant and a principal (who you act as) and to a set of permissions.
Free tier — no signup needed: Send a plain email to
futex-freepass@mail.rodmena.co.uk from any address. You'll receive a
futex_sk_... key back within seconds. Lost it? Email
futex-freepass+rotate@mail.rodmena.co.uk from the same address for a replacement.
Self-hosted / enterprise: Request a key from whoever operates this Futex instance. If you operate it, a tenant admin issues one:
# issue a submitter key for the "agent" principal in your tenant
curl -X POST https://futex.rodmena.co.uk/v1/tenants/<TENANT_ID>/api-keys \
-H "Authorization: Bearer <ADMIN_KEY>" \
-d '{"principal_id":"agent"}'
# -> {"id":"key_...","api_key":"futex_sk_...","principal_id":"agent"} (shown once)
Keep it secret — it authenticates everything you do. Export it so the tools/examples find it:
export FUTEX_API_KEY=futex_sk_...
A submitter/agent key needs hitl:decision:create + hitl:decision:read
(the agent_operator or tenant_admin role). A reviewer key needs
hitl:inbox:read + hitl:task:act. A 403 means the key lacks that permission.
You are being asked to connect this user to Futex. Complete these, asking only for what you cannot determine:
FUTEX_API_KEY environment variable. If unset, ask the user to paste their Futex key (section 1)..claude/skills/futex-hitl/SKILL.md (this project) or ~/.claude/skills/futex-hitl/SKILL.md (all projects), verbatim. It teaches you the full API even without MCP.futex-hitl tools load.Gives Claude first-class tools. User scope = available in every project after a restart. Replace <YOUR_API_KEY>:
claude mcp add --transport http --scope user futex-hitl \
https://futex.rodmena.co.uk/mcp/ \
--header "Authorization: Bearer <YOUR_API_KEY>"
Verify with claude mcp list (should show futex-hitl … ✔ Connected), then
restart Claude Code. Use /mcp inside Claude Code to see the tools. Which tools appear
depends on your key's permissions: submitter keys see the request/read tools, reviewer keys see the inbox/act tools.
.claude/skills/futex-hitl/SKILL.md)Copy this exactly. It is the portable way to teach Claude the API (works with or without MCP):
---
name: futex-hitl
description: Request durable, policy-driven human approvals through Futex (https://futex.rodmena.co.uk). Use whenever an action needs human sign-off before proceeding — deployments, payments, access grants, destructive ops — or to check the status of a pending approval, act on an approval task, or manage approval policies/webhooks/delegations.
---
# Futex HITL
Ask a human for permission via the Futex control plane. Base URL: `https://futex.rodmena.co.uk`.
## Authentication
Every request needs `Authorization: Bearer <FUTEX_API_KEY>`. Read the key from the
`FUTEX_API_KEY` environment variable; if unset, ask the user. Prefer the `futex-hitl` MCP tools if
configured; otherwise call the HTTP API with curl/httpx. Keys are tenant+principal scoped; a 403
means missing permission, a 401 means bad/expired key, a 503 means the auth service is unavailable
(fail closed — do not proceed as if approved).
## The one pattern to remember
1. Make sure a policy exists (`hitl_list_policies`). A policy says who approves and how.
2. Request the decision — NON-BLOCKING, returns a decision_id:
`hitl_request_decision(policy_id, idempotency_key, title, proposed_action{}, context{})`
3. Wait for the outcome by POLLING `hitl_get_decision(decision_id)` until `status` is terminal,
or register a webhook (push). Terminal = approved | rejected | changes_requested | timed_out | cancelled.
4. If `approved` -> proceed. If `rejected`/`changes_requested` -> read `feedback_for_agent`
(reasons + change requests) and revise or abort. NEVER proceed unless status == "approved".
Always pass a stable `idempotency_key` so a retry returns the same decision and never double-asks.
`proposed_action` and `context` are JSON objects (not strings). Do not block a user waiting for a
human — return the decision_id and poll, or come back when the webhook fires.
## MCP tools (submitter)
- hitl_list_policies() -> [{id,name}]
- hitl_get_policy(policy_id) -> {id,definition}
- hitl_list_reason_codes(policy_id) -> ["RISK", ...]
- hitl_request_decision(policy_id, idempotency_key, title, proposed_action{}, context{},
summary?, expires_in_hours?, webhook_url?) -> {id,status,context_hash}
- hitl_get_decision(decision_id) -> status, and on terminal the full outcome envelope
- hitl_cancel_decision(decision_id)
- hitl_submit_context_update(decision_id, context{})
## MCP tools (reviewer)
- hitl_list_inbox() -> tasks assigned to you
- hitl_get_task(task_id)
- hitl_act_on_task(task_id, action_type, reason_code?, reason_text?, justification?,
amendments?, target_principal_id?)
action_type: approve | reject | request_changes | abstain | amend_and_approve |
request_clearance | escalate | delegate_task
reject and request_changes REQUIRE reason_code + reason_text.
## HTTP equivalents
POST /v1/policies {name, definition} -> {id}
POST /v1/decisions {policy_id, idempotency_key, title, proposed_action, context,
summary?, expires_in_hours?, webhook_url?, runflow_binding_id?} -> {id,status}
GET /v1/decisions/{id} -> status; terminal reads return the outcome envelope
POST /v1/decisions/{id}/cancel
GET /v1/inbox/tasks -> {tasks:[...]}
POST /v1/tasks/{id}/actions {type, reason_code?, reason_text?, justification?, amendments?, target_principal_id?}
POST /v1/webhooks {url, secret, events:[...]} (push instead of poll)
POST /v1/delegations {delegate_id, mode:"ooo", start_at, end_at}
POST /v1/runflow/bindings {workflow_id, node_ref, credential} (gate a RunFlow node)
GET /v1/audit -> append-only audit events
## Policy definition (what a policy looks like)
{
"stages": [ // SEQUENTIAL stages; a stage's steps run in parallel
{"name":"Security","steps":[
{"name":"sec","mode":"any_of","assignees":["alice"]}]},
{"name":"Finance","steps":[
{"name":"fin","mode":"n_of_m","n_required":2,"assignees":["bob","cara","dan"],"sla":"PT4H"}]}
],
"reason_codes":["RISK","BUDGET"],
"require_reason_on_reject": true, // default true
"require_justification_on_approve": false,
"allow_one_click_approve": false, // adds an "Approve now" email link
"require_step_up": false, // email OTP before acting
"sod_rules": {"submitter_cannot_act": true},
"escalation_path": [{"after":"PT4H","action":"escalate_up","target":"manager"}],
"clearance": {"profile":{"key":"ciso","steps":[{"name":"sign","mode":"any_of","assignees":["ciso"]}]},
"trigger":{"mode":"always"}}, // further clearance before terminal approve
"amendments": {"allowed": true, "fields": ["/amount"]}, // enables amend_and_approve
"on_request_changes": "terminal", // terminal | hold
"expiration_action": "timed_out" // timed_out | auto_approve
}
step mode: all_of (everyone) | any_of (first) | n_of_m (needs n_required). SLAs are ISO-8601 (PT4H).
## Decision statuses
pending -> in_review -> (escalated) -> (approved_pending_clearance) ->
approved | rejected | changes_requested | timed_out | cancelled (last five are terminal)
## Terminal outcome envelope (from GET decision + decision.terminal webhook)
{"decision_id","tenant_id","status","policy_id","policy_version","context_hash",
"outcome":{"result","reasons":[{code,text,actor_id}],"justifications":[],"amendments":{},
"clearance":{required,completed,profile_ids},"delegation_summary":[]},
"feedback_for_agent":{"message","reason_codes":[],"change_requests":[]},
"audit_ref","terminal_at"}
## Webhooks (push)
POST /v1/webhooks {url, secret, events:["decision.created","task.assigned","task.completed",
"decision.escalated","decision.clearance_required","decision.terminal","decision.cancelled"]}
Each delivery is signed: verify X-HITL-Signature = "sha256=" + HMAC_SHA256(secret, f"{X-HITL-Timestamp}." + rawBody).
## Rules that always apply
- Non-blocking: creation returns immediately; poll or use webhooks. Never busy-wait a human.
- idempotency_key is REQUIRED on create and makes retries safe (same id, no double-ask).
- reject / request_changes require a reason_code + reason_text.
- one-click approve is only offered when the policy allows it AND no justification is required.
- Fail closed: on 503 (auth down) or timed_out, do NOT treat the request as approved.
- Errors use {"error":{code,message,details,request_id}}; include request_id when reporting problems.
Full guide: https://futex.rodmena.co.uk/claude
Base URL https://futex.rodmena.co.uk. Auth header Authorization: Bearer <key>
on every request. Bodies are JSON. Timestamps are RFC-3339 UTC. Identifiers are opaque and prefixed
(ten_, pol_, dec_, tsk_, act_, wh_…).
curl -X POST https://futex.rodmena.co.uk/v1/decisions \
-H "Authorization: Bearer $FUTEX_API_KEY" -H "Content-Type: application/json" \
-d '{
"policy_id": "pol_...",
"idempotency_key": "deploy-2026-07-24-a",
"title": "Deploy payments-api v1.2 to production",
"proposed_action": {"action":"deploy","service":"payments-api","version":"1.2.0"},
"context": {"risk":{"level":"high"},"change_ticket":"CHG-4821"},
"expires_in_hours": 72
}'
# -> {"id":"dec_...","status":"pending","context_hash":"sha256:...","expires_at":"..."}
curl https://futex.rodmena.co.uk/v1/decisions/dec_... -H "Authorization: Bearer $FUTEX_API_KEY"
# non-terminal -> {"status":"in_review", ...}
# terminal -> the full outcome envelope with status="approved" | "rejected" | ...
import os, time, uuid, httpx
api = httpx.Client(base_url="https://futex.rodmena.co.uk",
headers={"Authorization": f"Bearer {os.environ['FUTEX_API_KEY']}"}, timeout=30)
dec = api.post("/v1/decisions", json={
"policy_id": "pol_...", "idempotency_key": str(uuid.uuid4()),
"title": "Charge customer $4,200", "proposed_action": {"action":"charge","amount_usd":4200},
"context": {"customer":"cus_123"}}).json()
while True: # poll (or use a webhook instead)
d = api.get(f"/v1/decisions/{dec['id']}").json()
if d["status"] in ("approved","rejected","changes_requested","timed_out","cancelled"):
break
time.sleep(5)
if d["status"] == "approved":
... # proceed
else:
print(d.get("feedback_for_agent")) # why not — reasons + change requests
curl -X POST https://futex.rodmena.co.uk/v1/policies \
-H "Authorization: Bearer $FUTEX_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Prod deploy",
"definition":{"stages":[{"name":"Review","steps":[
{"name":"sec","mode":"any_of","assignees":["alice"]}]}],
"require_reason_on_reject":true,"allow_one_click_approve":true}}'
# -> {"id":"pol_...","name":"Prod deploy"}
curl https://futex.rodmena.co.uk/v1/inbox/tasks -H "Authorization: Bearer $REVIEWER_KEY"
curl -X POST https://futex.rodmena.co.uk/v1/tasks/tsk_.../actions \
-H "Authorization: Bearer $REVIEWER_KEY" -H "Content-Type: application/json" \
-d '{"type":"reject","reason_code":"RISK","reason_text":"Exposes SSH to the internet."}'
Humans normally act through the emailed link (a server-rendered review page), so an agent rarely needs the reviewer API — but it exists and behaves identically (REST, MCP, and the web all share one action engine).
POST /v1/webhooks
{"url":"https://your-app/hook","secret":"shared-secret",
"events":["decision.terminal","decision.clearance_required","task.assigned"]}
# each POST is signed: X-HITL-Signature = "sha256=" + HMAC_SHA256(secret, ts + "." + rawBody),
# with X-HITL-Timestamp, X-HITL-Event-Id, X-HITL-Event-Type headers.
GET /v1/webhooks/deliveries?state=dead # dead-letter queue
POST /v1/webhooks/deliveries/{id}/requeue # replay
| Term | Meaning |
|---|---|
| Policy | Versioned approval definition: stages, steps, quorum, SLA, reasons, clearance, SoD. In-flight decisions use an immutable snapshot. |
| Decision | One human-decision request under a policy. Non-blocking; has a status and, when terminal, an outcome envelope. |
| Task | One assignable unit of a decision step, assigned to a reviewer (or their delegate). |
| Action | A recorded act on a task: approve, reject, request_changes, amend_and_approve, request_clearance, escalate, delegate_task, abstain. |
| Clearance | A further sign-off after the primary approvals; terminal approve (and any RunFlow approve) are suppressed until it completes. |
| Delegation | Out-of-office rerouting of a principal's tasks to a delegate; actions record acting_as. |
| RunFlow binding | Optional: a decision gates a RunFlow workflow approval node — Futex calls approve/reject when the decision is terminal. |
idempotency_key in a tenant returns the same decision and never double-asks.403 means the key lacks that permission. Ask the operator for a broader key.503; expiries default to timed_out; do not proceed as if approved on either.429 includes Retry-After.reject and request_changes require reason_code + reason_text; the agent gets them back in feedback_for_agent.Futex · https://futex.rodmena.co.uk · multi-tenant human-in-the-loop approvals + MCP. See also the OpenAPI reference.