create-channel
Facilitates human-in-the-loop approval for AI workflows by creating a FinalApproval channel for actions requiring validation.
Install this skill
Security score
The create-channel skill was audited on May 13, 2026 and we found 89 security issues across 4 threat categories, including 5 high-severity. Review the findings below before installing.
Categories Tested
Security Issues
Template literal with variable interpolation in command context
| 27 | - Minimal cards like `<p>${data.subject}</p>` — wastes the reviewer's cognitive budget, looks unprofessional. |
Template literal with variable interpolation in command context
| 67 | ```bash |
Template literal with variable interpolation in command context
| 258 | If `$SCOPE_ENV` is not `production`, suffix the env var name with the environment so dev/staging/prod keys coexist cleanly: `FINALAPPROVAL_API_KEY_DEV`, `FINALAPPROVAL_API_KEY_STAGING`. This way the d |
Template literal with variable interpolation in command context
| 351 | ```html |
Template literal with variable interpolation in command context
| 376 | ```html |
Template literal with variable interpolation in command context
| 395 | ```html |
Template literal with variable interpolation in command context
| 434 | const response = await fetch(`${baseUrl}/api/v1/approvals`, { |
Template literal with variable interpolation in command context
| 437 | "Authorization": `Bearer ${process.env.FINALAPPROVAL_API_KEY}`, |
Template literal with variable interpolation in command context
| 441 | title: `Send email to ${data.recipient}`, |
Template literal with variable interpolation in command context
| 511 | .update(`${timestamp}.${body}`) |
Template literal with variable interpolation in command context
| 538 | console.log(`Denied: ${approval.title}`); |
Template literal with variable interpolation in command context
| 540 | console.log(`Reason: ${approval.denial_reason}`); |
Webhook reference - potential data exfiltration
| 9 | Wire human-in-the-loop approval into an agent's action. The end result: the agent's code submits a request, a human approves or denies in the dashboard, and the webhook fires back into the agent's cod |
Webhook reference - potential data exfiltration
| 15 | The FinalApproval API accepts two fields per approval: `body` (HTML the human sees) and `data` (JSON your webhook receives). Today the body is sent per-approval, but the intent is that **every approva |
Webhook reference - potential data exfiltration
| 22 | 4. **Data flows separately.** The `data` field carries the machine-readable payload the webhook returns to your code. Keep it clean JSON — don't duplicate HTML fragments into it, don't stringify objec |
Webhook reference - potential data exfiltration
| 44 | 2. **The runtime data fields** — the values that change per request. These define the TypeScript interface, the HTML template, and the webhook payload contract. If the action exists in the codebase, i |
Webhook reference - potential data exfiltration
| 45 | 3. **The webhook destination** — a publicly reachable HTTPS URL. If the user doesn't have one, help them get one (existing route, tunnel, scaffold, or serverless function) — see step 3. |
Webhook reference - potential data exfiltration
| 57 | - Propose, don't interrogate. "I see `sendEmail()` in `src/email.ts` — gating that one with fields `to/subject/body/priority`. Webhook URL?" beats a six-question form. |
Webhook reference - potential data exfiltration
| 215 | ### 3. Lock in the webhook URL |
Webhook reference - potential data exfiltration
| 222 | | **(b)** Local + tunnel | Run `ngrok http 3000` (or `cloudflared tunnel --url http://localhost:3000`), capture the `https://*.ngrok-free.app` URL, append `/webhooks/finalapproval`. Tell them they can |
Webhook reference - potential data exfiltration
| 223 | | **(c)** Scaffold Express | Use `http://localhost:3000/webhooks/finalapproval` only as a placeholder; immediately set up a tunnel (ngrok) to expose it. Without a public URL the channel can't deliver. |
Webhook reference - potential data exfiltration
| 224 | | **(d)** Serverless | Deploy the route first (Vercel `app/api/webhooks/finalapproval/route.ts`, Cloudflare Worker, etc.), capture the deployed URL, use that. | |
Webhook reference - potential data exfiltration
| 241 | \"webhook_url\": \"https://your-server.com/webhooks/finalapproval\", |
Webhook reference - potential data exfiltration
| 249 | - `webhook_secret` (`whsec_...`) — for verifying webhook signatures. Only returned when `webhook_url` is provided. **Shown once.** |
Webhook reference - potential data exfiltration
| 254 | The bearer token already lives at `~/.finalapproval/token.json`. The channel-scoped `fa_` key and webhook secret belong in the **project's** `.env`: |
Webhook reference - potential data exfiltration
| 264 | FINALAPPROVAL_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
Webhook reference - potential data exfiltration
| 267 | Both credentials are shown once and cannot be retrieved again. If lost, the API key requires creating a new channel; the webhook secret can be regenerated by updating the webhook URL in channel settin |
Webhook reference - potential data exfiltration
| 284 | "body": "<div class=\"space-y-3\"><div class=\"rounded-lg border p-3\"><p class=\"text-xs text-gray-500\">This is a test</p><p class=\"font-medium\">A real approval from your agent will look like this |
Webhook reference - potential data exfiltration
| 291 | Resolving the sample also exercises the webhook delivery path you configured in step 3 — useful for confirming end-to-end connectivity once step 7 is wired up. |
Webhook reference - potential data exfiltration
| 297 | 2. Includes the structured data for programmatic use in the webhook |
Webhook reference - potential data exfiltration
| 454 | - `body` is the HTML template (what the human sees). `data` is structured JSON (what the webhook returns to your code). Always include both. |
Webhook reference - potential data exfiltration
| 473 | // The webhook (step 7) fires when the human decides and runs the real action |
Webhook reference - potential data exfiltration
| 476 | ### 7. Wire up the webhook receiver — close the loop |
Webhook reference - potential data exfiltration
| 480 | When a human approves or denies, FinalApproval POSTs the decision to the webhook URL configured in step 3. **This is where the gated action actually runs.** |
Webhook reference - potential data exfiltration
| 482 | First, search the codebase for existing webhook handlers: |
Webhook reference - potential data exfiltration
| 483 | - Look for routes like `/webhooks`, `/api/hooks`, or similar patterns |
Webhook reference - potential data exfiltration
| 487 | If no webhook infrastructure exists, scaffold a receiver based on what was decided in step 3: |
Webhook reference - potential data exfiltration
| 491 | - **Serverless** — use the platform's HTTP handler signature (Vercel `app/api/webhooks/finalapproval/route.ts`, Cloudflare Worker `fetch()`, Lambda Function URL handler). The verification logic is ide |
Webhook reference - potential data exfiltration
| 500 | function verifyWebhook(headers: Record<string, string>, body: string): boolean { |
Webhook reference - potential data exfiltration
| 501 | const secret = process.env.FINALAPPROVAL_WEBHOOK_SECRET!; |
Webhook reference - potential data exfiltration
| 520 | // Webhook route |
Webhook reference - potential data exfiltration
| 521 | app.post("/webhooks/finalapproval", express.raw({ type: "application/json" }), (req, res) => { |
Webhook reference - potential data exfiltration
| 524 | if (!verifyWebhook(req.headers as Record<string, string>, rawBody)) { |
Webhook reference - potential data exfiltration
| 549 | **The webhook handler closes the loop.** The submission function (step 6) sends the request; the webhook handler (this step) receives the decision and runs the action. Both use `approval.data` as the |
Webhook reference - potential data exfiltration
| 555 | FinalApproval may retry webhook deliveries on failure (network blips, 5xx responses). Make the handler idempotent: track which `approval.id`s you've already processed (a Set in memory for ephemeral ha |
Webhook reference - potential data exfiltration
| 557 | #### Webhook payload schema |
Webhook reference - potential data exfiltration
| 590 | 4. **Webhook fires on approval** — approve the request in the dashboard, confirm the webhook handler receives it (check logs) and executes the action (the email actually sends, the deploy actually run |
Webhook reference - potential data exfiltration
| 591 | 5. **Webhook fires on denial** — deny a request with a reason, confirm the handler receives `denial_reason` and the action is *not* executed |
Webhook reference - potential data exfiltration
| 592 | 6. **Test from settings** — open the channel settings in the dashboard and click "Test" to send a synthetic webhook delivery. Confirm signature verification passes |
Webhook reference - potential data exfiltration
| 597 | - Webhook URL is reachable from the FinalApproval server |
Webhook reference - potential data exfiltration
| 598 | - Webhook secret matches (regenerate by updating the webhook URL in channel settings) |
Webhook reference - potential data exfiltration
| 626 | **Webhook handler:** `<path/to/webhook/route>` — executes on `approval.resolved` |
Webhook reference - potential data exfiltration
| 627 | **Secrets:** `FINALAPPROVAL_API_KEY`, `FINALAPPROVAL_WEBHOOK_SECRET` in `.env` |
Webhook reference - potential data exfiltration
| 640 | 4. **`body` is for humans, `data` is for code.** Never stringify objects into `body`. Never duplicate HTML fragments into `data`. The webhook handler reads `approval.data` — keep it clean JSON that ma |
Webhook reference - potential data exfiltration
| 641 | 5. **Handle both approve and deny paths in the webhook.** Denied requests must surface back to the caller (error, status row, notification) — silent drops leave the agent confused. |
Webhook reference - potential data exfiltration
| 642 | 6. **Webhook handler must be idempotent.** Track processed `approval.id`s; FinalApproval may retry on 5xx or network failure. |
Webhook reference - potential data exfiltration
| 649 | - `.cursor/rules/finalapproval.mdc`: wrap with YAML frontmatter (`---\ndescription: ...\nglobs: <paths>\n---`) scoped to the submission and webhook paths. |
Ngrok tunnel reference
| 222 | | **(b)** Local + tunnel | Run `ngrok http 3000` (or `cloudflared tunnel --url http://localhost:3000`), capture the `https://*.ngrok-free.app` URL, append `/webhooks/finalapproval`. Tell them they can |
Ngrok tunnel reference
| 223 | | **(c)** Scaffold Express | Use `http://localhost:3000/webhooks/finalapproval` only as a placeholder; immediately set up a tunnel (ngrok) to expose it. Without a public URL the channel can't deliver. |
Ngrok tunnel reference
| 490 | - **Local dev with tunnel (ngrok/cloudflared)** — add the same route to your local server; the tunnel forwards traffic to it |
Access to hidden dotfiles in home directory
| 61 | The rest of this skill needs a bearer token. Tokens are stored at `~/.finalapproval/token.json`. |
Access to hidden dotfiles in home directory
| 71 | if [ -f ~/.finalapproval/token.json ]; then |
Access to hidden dotfiles in home directory
| 72 | TOKEN=$(jq -r .token ~/.finalapproval/token.json) |
Access to hidden dotfiles in home directory
| 78 | [ -z "$CURRENT_EMAIL" ] && rm ~/.finalapproval/token.json |
Access to hidden dotfiles in home directory
| 85 | - If they want to switch → `rm ~/.finalapproval/token.json` and fall through to 2b. The device flow starts fresh, no extra confirmation. |
Access to hidden dotfiles in home directory
| 140 | mkdir -p ~/.finalapproval |
Access to hidden dotfiles in home directory
| 141 | echo "{\"token\":\"$TOKEN\"}" > ~/.finalapproval/token.json |
Access to hidden dotfiles in home directory
| 142 | chmod 600 ~/.finalapproval/token.json |
Access to hidden dotfiles in home directory
| 233 | TOKEN=$(jq -r .token ~/.finalapproval/token.json) |
Access to hidden dotfiles in home directory
| 254 | The bearer token already lives at `~/.finalapproval/token.json`. The channel-scoped `fa_` key and webhook secret belong in the **project's** `.env`: |
Access to hidden dotfiles in home directory
| 595 | - `~/.finalapproval/token.json` exists and the token still works (re-run step 2 if not) |
Access to .env file
| 254 | The bearer token already lives at `~/.finalapproval/token.json`. The channel-scoped `fa_` key and webhook secret belong in the **project's** `.env`: |
Access to .env file
| 256 | Check the developer's `.env` first — they may already have `FINALAPPROVAL_API_KEY` from a previous channel. Each channel gets its own key, so use a channel-specific name if multiple channels exist (e. |
Access to .env file
| 258 | If `$SCOPE_ENV` is not `production`, suffix the env var name with the environment so dev/staging/prod keys coexist cleanly: `FINALAPPROVAL_API_KEY_DEV`, `FINALAPPROVAL_API_KEY_STAGING`. This way the d |
Access to .env file
| 433 | const baseUrl = process.env.FINALAPPROVAL_URL ?? "https://www.finalapproval.ai"; |
Access to .env file
| 437 | "Authorization": `Bearer ${process.env.FINALAPPROVAL_API_KEY}`, |
Access to .env file
| 501 | const secret = process.env.FINALAPPROVAL_WEBHOOK_SECRET!; |
Access to .env file
| 596 | - `FINALAPPROVAL_API_KEY` is set in the project's `.env` (starts with `fa_`) |
Access to .env file
| 627 | **Secrets:** `FINALAPPROVAL_API_KEY`, `FINALAPPROVAL_WEBHOOK_SECRET` in `.env` |
External URL reference
| 63 | The default host is `https://www.finalapproval.ai`. Override with `FINALAPPROVAL_URL` (e.g. `http://localhost:3001` for local dev). |
External URL reference
| 68 | FINALAPPROVAL_URL="${FINALAPPROVAL_URL:-https://www.finalapproval.ai}" |
External URL reference
| 222 | | **(b)** Local + tunnel | Run `ngrok http 3000` (or `cloudflared tunnel --url http://localhost:3000`), capture the `https://*.ngrok-free.app` URL, append `/webhooks/finalapproval`. Tell them they can |
External URL reference
| 223 | | **(c)** Scaffold Express | Use `http://localhost:3000/webhooks/finalapproval` only as a placeholder; immediately set up a tunnel (ngrok) to expose it. Without a public URL the channel can't deliver. |
External URL reference
| 241 | \"webhook_url\": \"https://your-server.com/webhooks/finalapproval\", |
External URL reference
| 330 | - **Imagery:** `<img src="https://…" alt="…" loading="lazy" class="rounded-lg">` — HTTPS only, but it works. Use for brand marks, sender avatars (Gravatar), product thumbnails, OG previews. |
External URL reference
| 358 | <img src="https://www.gravatar.com/avatar/${gravatarHash}?s=64" alt="" class="rounded-full" loading="lazy" /> |
External URL reference
| 433 | const baseUrl = process.env.FINALAPPROVAL_URL ?? "https://www.finalapproval.ai"; |
External URL reference
| 663 | - **`<img src="https://…">`** — perfect for brand marks, sender avatars (Gravatar via `https://www.gravatar.com/avatar/<md5>`), product thumbnails, OG previews, screenshots hosted on your CDN. `alt` a |
External URL reference
| 680 | **Images:** `src` must use `https://`. Other schemes (`http://`, `data:`, relative paths) are stripped at render time. |