Futex HITL Claude setup

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.

Fastest start

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.

What Futex is (30 seconds)

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.

1. Get an API key

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.

2. Instructions for Claude

You are being asked to connect this user to Futex. Complete these, asking only for what you cannot determine:

Step 1 — key. Check the FUTEX_API_KEY environment variable. If unset, ask the user to paste their Futex key (section 1).
Step 2 — install the skill. Save the file in section 4 to .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.
Step 3 — register the MCP server (recommended). Run the command in section 3 with the user's key, then have them restart Claude Code so the futex-hitl tools load.
Step 4 — confirm. Summarise what you set up and offer a smoke test: list policies, then (if a policy exists) request a low-risk decision and poll it.

3. Add the MCP server

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.

4. Skill file (save as .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

5. API reference

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_…).

Request a decision (curl)

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":"..."}

Poll for the outcome

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" | ...

Request a decision (Python / httpx)

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

Create a policy

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"}

Reviewer side (act on a task)

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).

Webhooks (push instead of poll)

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

6. Concepts

TermMeaning
PolicyVersioned approval definition: stages, steps, quorum, SLA, reasons, clearance, SoD. In-flight decisions use an immutable snapshot.
DecisionOne human-decision request under a policy. Non-blocking; has a status and, when terminal, an outcome envelope.
TaskOne assignable unit of a decision step, assigned to a reviewer (or their delegate).
ActionA recorded act on a task: approve, reject, request_changes, amend_and_approve, request_clearance, escalate, delegate_task, abstain.
ClearanceA further sign-off after the primary approvals; terminal approve (and any RunFlow approve) are suppressed until it completes.
DelegationOut-of-office rerouting of a principal's tasks to a delegate; actions record acting_as.
RunFlow bindingOptional: a decision gates a RunFlow workflow approval node — Futex calls approve/reject when the decision is terminal.

7. Good to know


Futex · https://futex.rodmena.co.uk · multi-tenant human-in-the-loop approvals + MCP. See also the OpenAPI reference.