Skip to content
Back to skills

Hitl Approval

ASecurity

Implement human-in-the-loop approval flows. Use when adding Slack approvals, dashboard review queues, action gating, or trust-based auto-approval.

  • 666 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added February 9, 2026
businesstypescriptrustbashnextjsawsapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill hitl-approval --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Hitl Approval?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Hitl Approval
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-hitl-approval/badge)](https://www.skillsdirectory.com/skills/majiayu000-hitl-approval)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: hitl-approval
description: Implement human-in-the-loop approval flows. Use when adding Slack approvals, dashboard review queues, action gating, or trust-based auto-approval.
allowed-tools: Read, Grep, Glob, Edit, Write, Bash
---

# Human-in-the-Loop Approval

Core differentiator: **agent proposes, human approves**. Every sensitive action requires human approval until trust is established.

## Architecture Overview

```
┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Agent Tool     │ ──▶ │ requestApproval  │ ──▶ │  Slack Message  │
│  proposes       │     │ workflow         │     │  with buttons   │
└─────────────────┘     └──────────────────┘     └─────────────────┘
                                                          │
                                                          ▼
┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│  Action         │ ◀── │ executeApproved  │ ◀── │  Interactions   │
│  executed       │     │ Action workflow  │     │  handler        │
└─────────────────┘     └──────────────────┘     └─────────────────┘
```

## Approval Surfaces

| Surface | Use Case | Capabilities |
|---------|----------|--------------|
| **Slack** | Real-time push notifications | Approve/reject buttons, rejection reason modal |
| **Dashboard** | Bulk review, audit trail | Queue view, search, bulk actions |
| **Front Plugin** | In-context approval | View customer data, quick actions |

## Inngest Events

| Event | Trigger | Data |
|-------|---------|------|
| `support/approval.requested` | Agent proposes action | actionId, conversationId, appId, action, agentReasoning |
| `support/approval.decided` | Human clicks button | **actionId**, decision, decidedBy, decidedAt |
| `support/action.approved` | Human approves | actionId, approvedBy, approvedAt |
| `support/action.rejected` | Human rejects | actionId, rejectedBy, rejectedAt, reason? |

**IMPORTANT**: The `waitForEvent` match uses `data.actionId` - ensure all events use `actionId` consistently (not `approvalId`).

## Slack Block Kit Builder

Build approval messages with `buildApprovalBlocks`:

```typescript
import { buildApprovalBlocks } from '@skillrecordings/core/slack/approval-blocks'

const blocks = buildApprovalBlocks({
  actionId: 'action-123',
  conversationId: 'conv-456',
  appId: 'total-typescript',
  actionType: 'issue_refund',
  parameters: { orderId: 'order-789', amount: 99.00 },
  agentReasoning: 'Customer within 30-day refund window',
})
// Returns: header, reasoning section, parameters, approve/reject buttons
```

Button metadata structure (embedded in value):
```typescript
{ actionId, conversationId, appId }
```

## Slack Client (Lazy Init)

```typescript
import { postApprovalMessage, updateApprovalMessage } from '@skillrecordings/core/slack/client'

// Post approval message to channel
const { ts, channel } = await postApprovalMessage(
  process.env.SLACK_APPROVAL_CHANNEL,
  blocks,
  'Approval needed for Issue Refund'
)

// Update message after decision
await updateApprovalMessage(channel, ts, updatedBlocks, 'Approved')
```

## Slack Interactions Handler (Next.js App Router)

```typescript
// apps/slack/app/api/slack/interactions/route.ts
import { verifySlackSignature } from '../../../../lib/verify-signature'
import { inngest, SUPPORT_APPROVAL_DECIDED, SUPPORT_ACTION_APPROVED } from '@skillrecordings/core/inngest'

export async function POST(request: Request) {
  const body = await request.text()
  const signature = request.headers.get('x-slack-signature') ?? ''
  const timestamp = request.headers.get('x-slack-request-timestamp') ?? ''

  // Verify signature (HMAC-SHA256, 5-min replay protection)
  if (!verifySlackSignature({ signature, timestamp, body })) {
    return new Response('Invalid signature', { status: 401 })
  }

  // Parse URL-encoded payload
  const params = new URLSearchParams(body)
  const payload = JSON.parse(params.get('payload') ?? '{}')

  if (payload.type === 'block_actions') {
    const action = payload.actions[0]
    const metadata = JSON.parse(action.value)

    if (action.action_id === 'approve_action') {
      await inngest.send([
        { name: SUPPORT_APPROVAL_DECIDED, data: { approvalId: metadata.actionId, decision: 'approved', ... } },
        { name: SUPPORT_ACTION_APPROVED, data: { actionId: metadata.actionId, ... } },
      ])
    }
  }

  return new Response('OK', { status: 200 }) // Slack requires quick 200
}
```

## Signature Verification

```typescript
import { verifySlackSignature } from 'apps/slack/lib/verify-signature'

// Returns true if valid, false if invalid/expired
// Throws if SLACK_SIGNING_SECRET missing
const isValid = verifySlackSignature({
  signature: request.headers.get('x-slack-signature'),
  timestamp: request.headers.get('x-slack-request-timestamp'),
  body: rawRequestBody,
  // secret defaults to process.env.SLACK_SIGNING_SECRET
})
```

Key features:
- HMAC-SHA256 with timing-safe comparison (prevents timing attacks)
- 5-minute replay protection
- Validates v0= signature prefix

## Authority Levels

```typescript
// AUTO-APPROVE (execute immediately)
- Magic link requests
- Password reset requests
- Refunds within 30 days of purchase
- Transfers within 14 days of purchase
- Email/name updates

// REQUIRE-APPROVAL (draft action, wait for human)
- Refunds 30-45 days after purchase
- Transfers after 14 days
- Bulk seat management
- Account deletions

// ALWAYS-ESCALATE (flag for human, do not act)
- Angry/frustrated customers (sentiment detection)
- Legal language (lawsuit, lawyer, etc.)
- Repeated failed interactions
- Anything uncertain
```

## Draft Comparison (HITL Scoring)

Track how much humans modify agent drafts to build trust:

```typescript
function diffScore(draft: string, sent: string): 'unmodified' | 'edited' | 'rewritten' {
  const draftTokens = new Set(tokenize(draft))
  const sentTokens = new Set(tokenize(sent))
  const intersection = [...draftTokens].filter(t => sentTokens.has(t))
  const overlap = intersection.length / Math.max(draftTokens.size, sentTokens.size)

  if (overlap > 0.95) return 'unmodified'  // Trust++
  if (overlap > 0.50) return 'edited'       // Trust neutral
  return 'rewritten'                         // Trust--
}
```

## Trust Decay

Trust score uses exponential decay with 30-day half-life. High-trust agents can auto-send drafts.

## File Locations

| File | Purpose |
|------|---------|
| `packages/core/src/slack/approval-blocks.ts` | Block Kit message builder |
| `packages/core/src/slack/client.ts` | Slack WebClient singleton (lazy init) |
| `packages/core/src/inngest/workflows/request-approval.ts` | Approval request workflow |
| `packages/core/src/inngest/workflows/execute-approved-action.ts` | Execute after approval |
| `apps/slack/app/api/slack/interactions/route.ts` | Button click handler |
| `apps/slack/lib/verify-signature.ts` | HMAC signature verification |
| `packages/database/src/schema.ts` | ApprovalRequestsTable, ActionsTable |

## Environment Variables

| Variable | Purpose |
|----------|---------|
| `SLACK_BOT_TOKEN` | Slack WebClient authentication |
| `SLACK_SIGNING_SECRET` | Request signature verification |
| `SLACK_APPROVAL_CHANNEL` | Channel ID for posting approvals |

## Reference Docs

For full details, see:
- `docs/support-app-prd/66-hitl.md`

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.