AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Cloudflare Turnstile

skill-jackspace-claudeskillz-cloudflare-turnstile · by jackspace

|

No reviews yet
0 installs
29 views
0.0% view→install

Install

$ agentstack add skill-jackspace-claudeskillz-cloudflare-turnstile

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-jackspace-claudeskillz-cloudflare-turnstile)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
10mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Cloudflare Turnstile? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Cloudflare Turnstile

Status: Production Ready Last Updated: 2025-10-22 Dependencies: None (optional: @marsidev/react-turnstile for React) Latest Versions: @marsidev/react-turnstile@1.3.1, turnstile-types@1.2.3


Quick Start (10 Minutes)

1. Create Turnstile Widget

Get your sitekey and secret key from Cloudflare Dashboard.

# Navigate to: https://dash.cloudflare.com/?to=/:account/turnstile
# Create new widget → Copy sitekey (public) and secret key (private)

Why this matters:

  • Each widget has unique sitekey/secret pair
  • Sitekey goes in frontend (public)
  • Secret key ONLY in backend (private)
  • Use different widgets for dev/staging/production

2. Add Widget to Frontend

Embed the Turnstile widget in your HTML form.


  

  
    
    
    
    Submit
  

CRITICAL:

  • Never proxy or cache api.js - must load from Cloudflare CDN
  • Widget auto-creates hidden input cf-turnstile-response with token
  • Token expires in 5 minutes
  • Each token is single-use only

3. Validate Token on Server

ALWAYS validate the token server-side. Client-side verification alone is not secure.

// Cloudflare Workers example
export default {
  async fetch(request: Request, env: Env): Promise {
    const formData = await request.formData()
    const token = formData.get('cf-turnstile-response')
    const ip = request.headers.get('CF-Connecting-IP')

    // Validate token with Siteverify API
    const verifyFormData = new FormData()
    verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY)
    verifyFormData.append('response', token)
    verifyFormData.append('remoteip', ip)

    const result = await fetch(
      'https://challenges.cloudflare.com/turnstile/v0/siteverify',
      {
        method: 'POST',
        body: verifyFormData,
      }
    )

    const outcome = await result.json()

    if (!outcome.success) {
      return new Response('Invalid Turnstile token', { status: 401 })
    }

    // Token valid - proceed with form processing
    return new Response('Success!')
  }
}

The 3-Step Setup Process

Step 1: Create Widget Configuration

  1. Log into Cloudflare Dashboard
  2. Navigate to Turnstile section
  3. Click "Add Site"
  4. Configure:
  • Widget Mode: Managed (recommended), Non-Interactive, or Invisible
  • Domains: Add allowed hostnames (e.g., example.com, localhost for dev)
  • Name: Descriptive name (e.g., "Production Login Form")

Key Points:

  • Use separate widgets for dev/staging/production
  • Restrict domains to only those you control
  • Managed mode provides best balance of security and UX
  • localhost must be explicitly added for local testing

Step 2: Client-Side Integration

Choose between implicit or explicit rendering:

Implicit Rendering (Recommended for static forms):


function onSuccess(token) {
  console.log('Turnstile success:', token)
}

function onError(error) {
  console.error('Turnstile error:', error)
}

Explicit Rendering (For SPAs/dynamic UIs):

// 1. Load script with explicit mode

// 2. Render programmatically
const widgetId = turnstile.render('#container', {
  sitekey: 'YOUR_SITE_KEY',
  callback: (token) => {
    console.log('Token:', token)
  },
  'error-callback': (error) => {
    console.error('Error:', error)
  },
  theme: 'auto',
  execution: 'render', // or 'execute' for manual trigger
})

// Control lifecycle
turnstile.reset(widgetId)        // Reset widget
turnstile.remove(widgetId)       // Remove widget
turnstile.execute(widgetId)      // Manually trigger challenge
const token = turnstile.getResponse(widgetId) // Get current token

React Integration (using @marsidev/react-turnstile):

import { Turnstile } from '@marsidev/react-turnstile'

export function MyForm() {
  const [token, setToken] = useState()

  return (
    
       console.error(error)}
      />
      Submit
    
  )
}

Step 3: Server-Side Validation

MANDATORY: Always call Siteverify API to validate tokens.

interface TurnstileResponse {
  success: boolean
  challenge_ts?: string
  hostname?: string
  error-codes?: string[]
  action?: string
  cdata?: string
}

async function validateTurnstile(
  token: string,
  secretKey: string,
  options?: {
    remoteip?: string
    idempotency_key?: string
    expectedAction?: string
    expectedHostname?: string
  }
): Promise {
  const formData = new FormData()
  formData.append('secret', secretKey)
  formData.append('response', token)

  if (options?.remoteip) {
    formData.append('remoteip', options.remoteip)
  }

  if (options?.idempotency_key) {
    formData.append('idempotency_key', options.idempotency_key)
  }

  const response = await fetch(
    'https://challenges.cloudflare.com/turnstile/v0/siteverify',
    {
      method: 'POST',
      body: formData,
    }
  )

  const result = await response.json()

  // Additional validation
  if (result.success) {
    if (options?.expectedAction && result.action !== options.expectedAction) {
      return { success: false, 'error-codes': ['action-mismatch'] }
    }

    if (options?.expectedHostname && result.hostname !== options.expectedHostname) {
      return { success: false, 'error-codes': ['hostname-mismatch'] }
    }
  }

  return result
}

// Usage in Cloudflare Worker
const result = await validateTurnstile(
  token,
  env.TURNSTILE_SECRET_KEY,
  {
    remoteip: request.headers.get('CF-Connecting-IP'),
    expectedHostname: 'example.com',
  }
)

if (!result.success) {
  return new Response('Turnstile validation failed', { status: 401 })
}

Critical Rules

Always Do

Call Siteverify API - Server-side validation is mandatory ✅ Use HTTPS - Never validate over HTTP ✅ Protect secret keys - Never expose in frontend code ✅ Handle token expiration - Tokens expire after 5 minutes ✅ Implement error callbacks - Handle failures gracefully ✅ Use dummy keys for testing - Test sitekey: 1x00000000000000000000AASet reasonable timeouts - Don't wait indefinitely for validation ✅ Validate action/hostname - Check additional fields when specified ✅ Rotate keys periodically - Use dashboard or API to rotate secrets ✅ Monitor analytics - Track solve rates and failures

Never Do

Skip server validation - Client-side only = security vulnerability ❌ Proxy api.js script - Must load from Cloudflare CDN ❌ Reuse tokens - Each token is single-use only ❌ Use GET requests - Siteverify only accepts POST ❌ Expose secret key - Keep secrets in backend environment only ❌ Trust client-side validation - Tokens can be forged ❌ Cache api.js - Future updates will break your integration ❌ Use production keys in tests - Use dummy keys instead ❌ Ignore error callbacks - Always handle failures


Known Issues Prevention

This skill prevents 12 documented issues:

Issue #1: Missing Server-Side Validation

Error: Zero token validation in Turnstile Analytics dashboard Source: https://developers.cloudflare.com/turnstile/get-started/ Why It Happens: Developers only implement client-side widget, skip Siteverify call Prevention: All templates include mandatory server-side validation with Siteverify API

Issue #2: Token Expiration (5 Minutes)

Error: success: false for valid tokens submitted after delay Source: https://developers.cloudflare.com/turnstile/get-started/server-side-validation Why It Happens: Tokens expire 300 seconds after generation Prevention: Templates document TTL and implement token refresh on expiration

Issue #3: Secret Key Exposed in Frontend

Error: Security bypass - attackers can validate their own tokens Source: https://developers.cloudflare.com/turnstile/get-started/server-side-validation Why It Happens: Secret key hardcoded in JavaScript or visible in source Prevention: All templates show backend-only validation with environment variables

Issue #4: GET Request to Siteverify

Error: API returns 405 Method Not Allowed Source: https://developers.cloudflare.com/turnstile/migration/recaptcha Why It Happens: reCAPTCHA supports GET, Turnstile requires POST Prevention: Templates use POST with FormData or JSON body

Issue #5: Content Security Policy Blocking

Error: Error 200500 - "Loading error: The iframe could not be loaded" Source: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes Why It Happens: CSP blocks challenges.cloudflare.com iframe Prevention: Skill includes CSP configuration reference and check-csp.sh script

Issue #6: Widget Crash (Error 300030)

Error: Generic client execution error for legitimate users Source: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 Why It Happens: Unknown - appears to be Cloudflare-side issue (2025) Prevention: Templates implement error callbacks, retry logic, and fallback handling

Issue #7: Configuration Error (Error 600010)

Error: Widget fails with "configuration error" Source: https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578 Why It Happens: Missing or deleted hostname in widget configuration Prevention: Templates document hostname allowlist requirement and verification steps

Issue #8: Safari 18 / macOS 15 "Hide IP" Issue

Error: Error 300010 when Safari's "Hide IP address" is enabled Source: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 Why It Happens: Privacy settings interfere with challenge signals Prevention: Error handling reference documents Safari workaround (disable Hide IP)

Issue #9: Brave Browser Confetti Animation Failure

Error: Verification fails during success animation Source: https://github.com/brave/brave-browser/issues/45608 (April 2025) Why It Happens: Brave shields block animation scripts Prevention: Templates handle success before animation completes

Issue #10: Next.js + Jest Incompatibility

Error: @marsidev/react-turnstile breaks Jest tests Source: https://github.com/marsidev/react-turnstile/issues/112 (Oct 2025) Why It Happens: Module resolution issues with Jest Prevention: Testing guide includes Jest mocking patterns and dummy sitekey usage

Issue #11: localhost Not in Allowlist

Error: Error 110200 - "Unknown domain: Domain not allowed" Source: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes Why It Happens: Production widget used in development without localhost in allowlist Prevention: Templates use dummy test keys for dev, document localhost allowlist requirement

Issue #12: Token Reuse Attempt

Error: success: false with "token already spent" error Source: https://developers.cloudflare.com/turnstile/troubleshooting/testing Why It Happens: Each token can only be validated once Prevention: Templates document single-use constraint and token refresh patterns


Configuration Files Reference

wrangler.jsonc (Cloudflare Workers)

{
  "name": "my-app",
  "main": "src/index.ts",
  "compatibility_date": "2025-10-22",

  // Public sitekey (safe to commit)
  "vars": {
    "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" // Use real key in production
  },

  // Secret key (DO NOT commit - use wrangler secret)
  // Run: wrangler secret put TURNSTILE_SECRET_KEY
  "secrets": ["TURNSTILE_SECRET_KEY"]
}

Why these settings:

  • vars for public sitekey (visible in client code)
  • secrets for private secret key (encrypted, backend-only)
  • Use dummy keys for development (see testing-guide.md)
  • Rotate production secret keys quarterly

Required CSP Directives


Common Patterns

Pattern 1: Hono + Cloudflare Workers

import { Hono } from 'hono'

type Bindings = {
  TURNSTILE_SECRET_KEY: string
  TURNSTILE_SITE_KEY: string
}

const app = new Hono()

app.post('/api/login', async (c) => {
  const body = await c.req.formData()
  const token = body.get('cf-turnstile-response')

  if (!token) {
    return c.text('Missing Turnstile token', 400)
  }

  // Validate token
  const verifyFormData = new FormData()
  verifyFormData.append('secret', c.env.TURNSTILE_SECRET_KEY)
  verifyFormData.append('response', token.toString())
  verifyFormData.append('remoteip', c.req.header('CF-Connecting-IP') || '')

  const verifyResult = await fetch(
    'https://challenges.cloudflare.com/turnstile/v0/siteverify',
    {
      method: 'POST',
      body: verifyFormData,
    }
  )

  const outcome = await verifyResult.json()

  if (!outcome.success) {
    return c.text('Invalid Turnstile token', 401)
  }

  // Process login
  return c.json({ message: 'Login successful' })
})

export default app

When to use: API routes in Cloudflare Workers with Hono framework

Pattern 2: React + Next.js App Router

'use client'

import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'

export function ContactForm() {
  const [token, setToken] = useState()
  const [error, setError] = useState()

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault()

    if (!token) {
      setError('Please complete the challenge')
      return
    }

    const formData = new FormData(e.currentTarget)
    formData.append('cf-turnstile-response', token)

    const response = await fetch('/api/contact', {
      method: 'POST',
      body: formData,
    })

    if (!response.ok) {
      setError('Submission failed')
      return
    }

    // Success
  }

  return (
    
      
      

       setError('Challenge failed')}
        onExpire={() => setToken(undefined)}
      />

      {error && {error}}

      
        Submit
      
    
  )
}

When to use: Client-side forms in Next.js with React hooks

Pattern 3: E2E Testing with Dummy Keys

// test/helpers/turnstile.ts
export const TEST_TURNSTILE = {
  sitekey: {
    alwaysPass: '1x00000000000000000000AA',
    alwaysBlock: '2x00000000000000000000AB',
    invisible: '1x00000000000000000000BB',
    interactive: '3x00000000000000000000FF',
  },
  secretKey: {
    alwaysPass: '1x0000000000000000000000000000000AA',
    alwaysFail: '2x0000000000000000000000000000000AA',
    tokenSpent: '3x0000000000000000000000000000000AA',
  },
  dummyToken: 'XXXX.DUMMY.TOKEN.XXXX',
}

// Playwright test example
test('form submission with Turnstile', async ({ page }) => {
  // Set test environment
  await page.goto('/contact?test=true')

  // Widget uses test sitekey in test mode
  await page.fill('input[name="email"]', 'test@example.com')

  // Turnstile auto-solves with dummy token
  await page.click('button[type="submit"]')

  await expect(page.locator('.success')).toBeVisible()
})

When to use: Automated testing (Playwright, Cypress, Jest)

Pattern 4: Widget Lifecycle Management

class TurnstileManager {
  private widgetId: string | null = null
  private sitekey: string

  constructor(sitekey: string) {
    this.sitekey = sitekey
  }

  render(containerId: string, callbacks: {
    onSuccess: (token: string) => void
    onError: (error: string) => void
  }) {
    if (this.widgetId !== null) {
      this.reset() // Reset if already rendered
    }

    this.widgetId = turnstile.render(containerId, {
      sitekey: this.sitekey,
      callback: callbacks.onSuccess,
      'error-callback': callbacks.onError,
      'expired-callback': () => this.reset(),
    })

    return this.widgetId
  }

  reset() {
    if (this.widgetId !== null) {
      turnstile.reset(this.widgetId)
    }
  }

  remove() {
    if (this.widgetId !== null) {
      turnstile.remove(this.widgetId)
      this.widgetId = null
    }
  }

  getToken(): string | undefined {
    if (this.widgetId === null) return undefined
    return turnstile.getRe

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [jackspace](https://github.com/jackspace)
- **Source:** [jackspace/ClaudeSkillz](https://github.com/jackspace/ClaudeSkillz)
- **License:** MIT
- **Homepage:** http://claudeskillz.jackspace.com/

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.