Skip to content

Use Cases & Patterns

Every HumanAuth request authorizes exactly one action. You target a single human (hu_…) or a group (grp_…), and the platform returns a signed receipt once the decision is made. The richer patterns below — quorums, escalation, batch review, CI/CD gates — are compositions of that one primitive plus the platform features (groups, webhooks, the receipt ledger) built to support them.

Every pattern keeps the same shape: request an authorization, wait for the decision, then verify the receipt at the system that performs the action. The request asks; the verified receipt is what actually gates the irreversible step. Snippets below use @humanauth/sdk and the humanauth CLI as shipped.

The default flow. One agent, one human. The agent asks; the human approves or denies on their device.

When to use: Individual developers using AI agents, personal automation, small teams with a single point of contact.

Example: A developer running Claude Code locally. The agent wants to execute rm -rf /tmp/build. HumanAuth sends the request, the developer reviews on their phone, approves with Touch ID.

import { HumanAuth } from "@humanauth/sdk";
const auth = new HumanAuth({ apiKey: process.env.HUMANAUTH_API_KEY! });
const req = await auth.authorize("hu_karthick_7x2f", {
action: "bash:execute",
description: "Remove temporary build directory (rm -rf /tmp/build) — cache is stale, 2GB reclaimable",
severity: "medium",
ttl: 300,
});
const result = await req.wait(); // resolves when the human decides, or on expiry
if (result.result === "approved") {
const jws = await auth.receipts.getJws(result.receipt_id!);
// Hand `jws` to the backend that runs the command; it re-verifies before acting.
await execCommand("rm -rf /tmp/build", jws);
}

The action label must match the platform’s service:operation convention (e.g. bash:execute, deployments:create) — the same label the verifier checks. HumanAuth has no free-form parameters field: everything the human needs to judge the request goes in description, and that text is what the receipt’s plan hash binds.

Two or three people must sign off before an action proceeds. This is a platform primitive, not something you assemble yourself: create a group with a quorum or unanimous policy, then authorize the group. The platform fans the request out, applies the policy, and mints a single receipt naming every approver once the rule is satisfied.

When to use: Financial transactions over a threshold, production deployments, compliance-sensitive operations that require multiple sign-offs.

Architecture:

Agent ── authorize(grp_deploy_approvers) ──> HumanAuth platform
| fan-out to group members
|-- hu_cfo (CFO) --> APPROVED
|-- hu_cto (CTO) --> APPROVED
+-- hu_vpeng (VP Eng) --> DENIED
|
policy: quorum = 2 of 3 --------+ --> APPROVED, one receipt minted

Implementation:

Terminal window
humanauth groups create "deploy-approvers" \
--members hu_cfo_1a2b,hu_cto_3c4d,hu_vpeng_5e6f \
--quorum 2
# -> grp_deploy_approvers (2-of-3 quorum)
# Require every member instead of a quorum:
humanauth groups create "wire-approvers" \
--members hu_cfo_1a2b,hu_cto_3c4d \
--unanimous

Considerations:

  • The platform enforces the policy (--quorum N or --unanimous). Your code never counts votes — it reads one terminal result.result.
  • The receipt carries every approver’s hardware-bound device co-signature over the same plan hash: cryptographic non-repudiation, per approver.
  • result.decisions lists each member’s verdict, device, and any reason — useful for audit even when the quorum is already met.
  • You can also create groups from the SDK: auth.groups.create({ name, members, policy: { type: "quorum", quorum: 2 } }).

The agent sends a request to the primary approver first. If they do not respond within a shorter TTL, your code escalates to the next person in the chain.

When to use: On-call workflows, time-sensitive operations, manager escalation for unanswered requests.

Architecture:

Agent
|-- authorize(hu_oncall_eng, TTL=120s)
| +-- EXPIRED (no response in 2 min)
|-- authorize(hu_team_lead, TTL=180s)
| +-- APPROVED
+-- Done

Implementation:

import { HumanAuth } from "@humanauth/sdk";
const auth = new HumanAuth({ apiKey: process.env.HUMANAUTH_API_KEY! });
const chain = [
{ humanId: "hu_oncall_eng", ttl: 120 },
{ humanId: "hu_team_lead", ttl: 180 },
{ humanId: "hu_eng_director", ttl: 300 },
];
for (const { humanId, ttl } of chain) {
const req = await auth.authorize(humanId, {
action: "deployments:hotfix",
description: humanId === chain[0].humanId
? "Deploy security hotfix to production (auth v1.2.1-hotfix)"
: `Escalated from ${chain[0].humanId}: deploy security hotfix to production (auth v1.2.1-hotfix)`,
severity: "critical",
ttl,
});
const result = await req.wait();
if (result.result === "approved") {
const jws = await auth.receipts.getJws(result.receipt_id!);
await deployHotfix(jws);
break;
}
if (result.result === "denied") {
// An explicit denial stops the chain — do not escalate a "no".
console.log(`Denied by ${humanId}: ${result.decisions[0]?.reason ?? "no reason given"}`);
break;
}
// Expired — nobody decided in time; move to the next person.
console.log(`${humanId} did not respond within ${ttl}s. Escalating...`);
}

Considerations:

  • Explicit denials should stop the chain — do not escalate a denied request.
  • Put the escalation context in description so the next approver knows why they are being asked.
  • Give the chain a maximum depth (usually 3–4 levels).

The agent collects several pending actions and sends them as one request for batch review. This reduces notification fatigue for non-urgent operations.

When to use: Nightly batch jobs, agents that queue up non-urgent actions for periodic human review.

Architecture:

Agent
+-- authorize(hu_ops_lead):
action: "ops:batch_approve"
description: "Overnight batch — approve or deny as a whole:
1. Delete /tmp/old-logs (2.3 GB)
2. Run migration users_v3 (15,000 rows)
3. Deploy api v2.1.0"

Implementation:

import { HumanAuth } from "@humanauth/sdk";
const auth = new HumanAuth({ apiKey: process.env.HUMANAUTH_API_KEY! });
const pendingActions = [
"Delete /tmp/old-logs (2.3 GB)",
"Run migration users_v3 (15,000 rows)",
"Deploy api v2.1.0",
];
const req = await auth.authorize("hu_ops_lead", {
action: "ops:batch_approve",
description:
"Overnight batch — approve or deny as a whole:\n" +
pendingActions.map((a, i) => `${i + 1}. ${a}`).join("\n"),
severity: "high",
ttl: 600,
});
const result = await req.wait();
if (result.result === "approved") {
const jws = await auth.receipts.getJws(result.receipt_id!);
for (const action of pendingActions) {
await executeAction(action, jws);
}
}

Considerations:

  • One request is one decision — all-or-nothing. The human approves or denies the whole batch; there is no per-item verdict. For per-item granularity, send one request per item.
  • HumanAuth has no structured parameters field. Summarize the batch in description — that is what the approver reads and what the receipt’s plan hash binds. Keep it scannable.

Agents running in CI/CD pipelines, cron jobs, or multi-agent orchestration systems. No human watches a terminal — HumanAuth provides the approval channel via push notifications.

When to use: Overnight batch jobs, scheduled agents, CI/CD deployment gates, multi-agent systems that need human sign-off for critical steps.

Architecture:

CI/CD Pipeline
+-- Agent step: "deploy to production"
+-- authorize(hu_devops_lead) --> DevOps lead's phone
+-- Push notification at 3 AM
+-- Approve from bed --> deploy proceeds

Implementation (GitHub Actions example):

.github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Authenticate the HumanAuth CLI
run: npx @humanauth/cli login --api-key "$HUMANAUTH_API_KEY"
env:
HUMANAUTH_API_KEY: ${{ secrets.HUMANAUTH_API_KEY }}
- name: Request deployment approval via HumanAuth
run: |
npx @humanauth/cli test hu_devops_lead \
--action deployments:create \
--description "Deploy ${{ github.sha }} to production" \
--ttl 600
- name: Deploy
run: ./scripts/deploy.sh

Considerations:

  • humanauth login --api-key writes the key to ~/.humanauth/config.json; later commands read it from there (the CLI does not read HUMANAUTH_API_KEY directly).
  • Push notifications are critical — the human is not watching anything.
  • Set generous TTL values since response time may be slower at off-hours.
  • Handle expiry gracefully: retry later, alert via a backup channel, or fail safe.

Most snippets in this guide reach for req.wait() as if the decision lands promptly. Two clocks say otherwise. An LLM tool call is expected back in seconds; a human approval takes minutes to hours — someone has to see the push, pick up their phone, and decide. When those clocks diverge, the request→act loop has to split.

When to use: Any approval that may outlive a single agent turn — off-hours deploys, high-value transfers, anything routed to a person who is not watching a terminal.

Which shape you need comes down to whether the human is there right now.

Pattern A — human present. The person is expected to answer within the turn’s budget, so one tool call can carry the whole loop: fire POST /v1/authorize, poll GET /v1/requests/:id with bounded backoff, and once it is decided the same response hands back the receipt to verify and act on. req.wait() (SDK) and waitForReceipt() (verifier) both package exactly this — polling is the floor every integration gets for free.

Pattern B — human away. When the wait can run to hours, no single turn should hold it open. The asking turn fires the request, hands the request_id to a durable runner, and ends. The runner waits — polling is the floor, but a request.decided webhook is the durable upgrade that removes the idle poll — then resumes the agent, possibly in a fresh process, once the decision lands. The receipt is a bearer proof: whatever process holds it can verify and act, so the acting turn need not be the asking one.

Considerations:

  • Polling is the floor; the webhook is the upgrade. Bounded polling needs nothing extra and fits Pattern A. For Pattern B, subscribe to request.decided so the runner wakes on the decision instead of burning cycles — see the Webhooks guide.
  • The receipt is portable. Because verification is offline and the receipt is a bearer proof, the process that acts need not be the one that asked. Carry the JWS, not live state.
  • Thread an idempotencyKey across the split. A durable resume can run twice (retries, webhook redeliveries). The verifier’s atomic (jti, idempotencyKey) claim is what makes double-execution impossible — pass the same key from ask to act.

One approver app on the person’s phone, reachable by many agents. A person installs the approver once, enrolls a device, and can be rostered (hu_…) in any number of tenants — each with its own agent and its own ha_… API key.

When to use: Any person with multiple AI agent integrations — personal laptop, work agents, home automation, trading bots.

Architecture:

Approver App (one person's phone)
|-- Requests from: Claude Code (personal tenant, key ha_live_A…)
|-- Requests from: Company AI Agent (work tenant, key ha_live_B…)
|-- Requests from: Home Automation (home tenant, key ha_live_C…)
+-- Requests from: Trading Bot (trading tenant, key ha_live_D…)

Each device holds its own hardware-bound key (generated inside the Secure Enclave, non-exportable). Each tenant holds its own API key. Compromising one agent lets it forge nothing — it cannot mint a receipt (only the person’s device co-signs), and it cannot see another tenant’s requests.

Inspect and manage (admin side):

Terminal window
# Who is rostered in this tenant, and what devices have they enrolled?
humanauth humans list
humanauth devices list hu_alex_7x2f
# Revoke this person's authority in this tenant:
humanauth humans remove hu_alex_7x2f

The person denies a request with a reason, and the agent adapts based on the feedback. Denial is not a dead end — it is a course correction.

When to use: Interactive agent workflows where the human wants to guide the agent rather than simply block it.

Architecture:

Agent: "Run: rm -rf /home/user/projects"
+-- authorize(hu_karthick_7x2f)
+-- DENIED, reason: "Too broad. Only delete /home/user/projects/tmp"
+-- Agent adapts: "Run: rm -rf /home/user/projects/tmp"
+-- authorize(hu_karthick_7x2f)
+-- APPROVED

Implementation:

import { HumanAuth } from "@humanauth/sdk";
const auth = new HumanAuth({ apiKey: process.env.HUMANAUTH_API_KEY! });
let command = "rm -rf /home/user/projects";
let lastReason: string | null = null;
const MAX_RETRIES = 3;
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
const req = await auth.authorize("hu_karthick_7x2f", {
action: "bash:execute",
description: attempt === 0
? `Execute: ${command}`
: `Execute (adjusted after feedback "${lastReason}"): ${command}`,
severity: "high",
ttl: 300,
});
const result = await req.wait();
if (result.result === "approved") {
const jws = await auth.receipts.getJws(result.receipt_id!);
await execCommand(command, jws);
break;
}
// A single-human denial carries the human's reason on the decision record.
const reason = result.decisions[0]?.reason ?? null;
if (result.result !== "denied" || !reason) {
console.log(`Stopping (${result.result}${reason ? "" : ", no feedback"}).`);
break;
}
// Pass the denial reason to the LLM to adapt.
lastReason = reason;
command = await llm.adjustCommand(command, reason);
console.log(`Adjusted command to: ${command}`);
}

Considerations:

  • The human’s reason rides on result.decisions[].reason — no extra call needed.
  • Set a maximum retry count to prevent infinite loops.
  • Each retry is a fresh request with a new request_id (and, if approved, its own receipt).

Run the auditor test on any approval flow: an examiner points at one agent action from last quarter and asks, “Prove who approved this — and that they approved these exact parameters.” If the answer is a Slack screenshot and a database row, you are asking the examiner to trust the integrity of the very systems under audit. A receipt answers differently: here is a signed artifact, here are the published public keys — check it yourself. humanauth receipts get rct_… --verify reproduces the full proof chain on the examiner’s own laptop, with HumanAuth unreachable and your database out of the loop. The approver’s hardware-held key signed those parameters; nobody — not us, not you, not a database admin — can re-sign a different history.

The platform is the system of record. Every decided request mints a signed receipt in the tenant’s receipt ledger — no local audit files to manage, no ~/.harp directory. Each receipt is a compact JWS you can verify offline against the published JWKS, so proof of a human decision survives independently of HumanAuth’s uptime.

When to use: SOC2, HIPAA, SOX, or any environment requiring proof of human authorization for agent actions.

Terminal window
# The receipt ledger — every decided request lands here.
humanauth receipts list --since 2026-04-01 --limit 50
# Inspect one receipt's decoded JWS payload (no signature check):
humanauth receipts get rct_9f2c… --decode
# Or run the full verifier pipeline against it:
humanauth receipts get rct_9f2c… --verify \
--audience ten_yourtenant \
--action deployments:create \
--plan-file plan.json

For real-time delivery to a SIEM (Splunk, Datadog, …), subscribe to the request.decided webhook — the receipt JWS rides inside the event payload. See the Webhook Subscriptions guide.

Considerations:

  • The ledger is the source of truth. Receipts are portable and verifiable without contacting HumanAuth — only the published JWKS and the receipt itself are needed.
  • Non-repudiation is cryptographic: each receipt carries the platform’s signature plus every approver’s hardware-bound device co-signature, bound to the exact action and plan hash.
  • Denied and expired outcomes are recorded too. A denied request mints a receipt whose result is denied; on TTL expiry no receipt is minted, but the outcome is still visible via humanauth requests get and the request.expired webhook — so your agent is never left silently blocked.