Ensures that when an AI agent requests permission for a sensitive action, the approval is cryptographically proven to come from a verified human — not from the agent itself, an automation script, or a replay attack.
AI agents operating autonomously can self-approve their own tool actions. Even "human-in-the-loop" systems that prompt for approval can be bypassed if the approval check happens within the agent's own process. There's no cryptographic proof that a human actually responded.
┌─────────────────────────────────────────────────────────────────────┐
│ Trust Chain │
│ │
│ Keycloak OAuth ──► verified human accounts only │
│ │ │
│ ▼ │
│ Browser Extension ──► holds Ed25519 keypair in memory │
│ (chrome.storage.session, extractable:false) │
│ │ │
│ ▼ │
│ User clicks Approve ──► extension signs the challenge │
│ │ │
│ ▼ │
│ Tool verifies signature against Keycloak-registered public key │
│ │ │
│ ▼ │
│ APPROVED — cryptographic proof that a registered human responded │
└─────────────────────────────────────────────────────────────────────┘
The agent cannot forge a valid signature. Only the browser extension, which holds the private key (never extractable, cleared on browser close), can sign challenges.
Tool/CLI/MCP/Skill
│
│ POST /sign {challenge}
▼
Native Messaging Host (localhost:7331)
│
│ NativeMessaging stdio protocol
▼
Extension Background Service Worker
│
│ Show popup to user
▼
User: Approve / Deny
│
│ Ed25519 sign(challenge) ← private key stays in SubtleCrypto
▼
Extension → NativeHost → Tool
│
│ GET /admin/realms/hitl/users/{id}
▼
Keycloak Admin API
│
│ returns ed25519_public_key attribute
▼
Tool verifies signature
│
▼
APPROVED or DENIED
| Property | How it's enforced |
|---|---|
| Only verified humans can approve | Keycloak realm — no bot/service accounts allowed |
| Agent cannot self-approve | Private key never leaves the browser extension (extractable:false) |
| Key cleared when browser closes | chrome.storage.session — in-memory only |
| Replay attacks prevented | Per-challenge nonce tracked in Redis + timestamp TTL |
| Key bound to authenticated user | Public key registered in Keycloak at OAuth login |
| Approval is action-specific | Challenge includes the action description, nonce, timestamp |
- If the browser host OS is fully compromised (OS-level keylogger), the private key can be captured at signing time
- If Keycloak is compromised, public keys can be swapped
- The 5-minute TTL (configurable) means the human must be present; long-running agents need to re-request
hitl/
├── CLAUDE.md # Agent/developer instructions
├── README.md # This file
├── .env.example # Environment variable template
├── docker-compose.yml # Keycloak + Redis
├── oauth/ # Keycloak realm config + init scripts
├── extension/ # Browser extension (TypeScript, MV3, Chrome+Firefox)
├── cli/ # Demo CLI: hitl request --action "..."
├── tool/ # @require_human_approval decorator demo
├── skill/ # Claude Code /hitl-approve slash command
└── mcp/ # FastMCP server with request_approval tool
- Docker + Docker Compose
- Node.js 20+
- Python 3.11+
- uv Python package manager
- Chrome 113+ or Firefox 115+
cp .env.example .env
# Edit .env if needed (defaults work for local dev)
docker compose up -dKeycloak will be available at http://localhost:8080. The realm hitl is imported automatically on first start.
cd extension
npm install
npm run buildChrome: Go to chrome://extensions → Enable "Developer mode" → "Load unpacked" → select extension/dist-chrome/
Firefox: Go to about:debugging → "This Firefox" → "Load Temporary Add-on" → select extension/dist-firefox/manifest.json
cd extension
node signing-host/install.jsThis installs the native messaging host manifest so the extension can communicate with the local HTTP relay.
cd /path/to/hitl
uv pip install -e cli/ tool/ skill/ mcp/Click the extension icon in the browser toolbar → "Login with Keycloak" → register with a Keycloak account. Your Ed25519 keypair is generated and your public key is stored in Keycloak.
hitl request --action "delete /tmp/testfile"A popup appears in the browser. Click Approve. The terminal prints:
APPROVED — signature verified
User: alice@example.com
Challenge: a3f1b2... (nonce truncated)
oauth/README.md— Keycloak setup and configurationextension/README.md— Browser extension developmentcli/README.md— CLI usage and integration APItool/README.md—@require_human_approvaldecoratorskill/README.md— Claude Code skill usagemcp/README.md— MCP server configuration
# Run all tests
cd extension && npm test
cd cli && uv run pytest
cd tool && uv run pytest
cd mcp && uv run pytest
# Lint
cd extension && npm run lint
cd cli && uv run ruff check . && uv run mypy hitl_cli/Apache 2.0 — see LICENSE.