Claude-skills cloudflare-turnstile
This skill should be used when the user asks to "add turnstile", "implement bot protection", "validate turnstile token", "fix turnstile error", "setup captcha alternative", or encounters error codes 100*/300*/600*, CSP errors, or token validation failures. Provides CAPTCHA-alternative protection for Cloudflare Workers, React, Next.js, and Hono.
git clone https://github.com/secondsky/claude-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/secondsky/claude-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/cloudflare-turnstile/skills/cloudflare-turnstile" ~/.claude/skills/secondsky-claude-skills-cloudflare-turnstile && rm -rf "$T"
plugins/cloudflare-turnstile/skills/cloudflare-turnstile/SKILL.mdCloudflare Turnstile
Status: Production Ready ✅ | Last Verified: 2025-11-26
Dependencies: None (optional: @marsidev/react-turnstile for React)
Contents: Quick Start • Critical Rules • Top 12 Errors • Common Patterns • When to Load References • Troubleshooting
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.
<!DOCTYPE html> <html> <head> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> </head> <body> <form id="myForm" action="/submit" method="POST"> <input type="email" name="email" required> <!-- Turnstile widget renders here --> <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div> <button type="submit">Submit</button> </form> </body> </html>
CRITICAL:
- Never proxy or cache
- must load from Cloudflare CDNapi.js - Widget auto-creates hidden input
with tokencf-turnstile-response - 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<Response> { 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
- Log into Cloudflare Dashboard
- Navigate to Turnstile section
- Click "Add Site"
- 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):
<!-- 1. Load script --> <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script> <!-- 2. Add widget --> <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-callback="onSuccess" data-error-callback="onError"></div> <script> function onSuccess(token) { console.log('Turnstile success:', token) } function onError(error) { console.error('Turnstile error:', error) } </script>
Explicit Rendering (For SPAs/dynamic UIs):
// 1. Load script with explicit mode <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" defer></script> // 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<string>() return ( <form> <Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} onError={(error) => console.error(error)} /> <button disabled={!token}>Submit</button> </form> ) }
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<TurnstileResponse> { 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<TurnstileResponse>() // 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:
1x00000000000000000000AA
✅ Set 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
✅ Validate token AFTER form submission - Verify tokens after user completes form, not before. Premature validation creates security vulnerabilities where attackers obtain valid tokens then bypass protection
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
Wrangler (Workers): Load
templates/wrangler-turnstile-config.jsonc for complete configuration. Key settings: vars for public sitekey (safe to commit), secrets for private secret key (use wrangler secret put TURNSTILE_SECRET_KEY).
CSP Directives (if using Content Security Policy):
<meta http-equiv="Content-Security-Policy" content=" script-src 'self' https://challenges.cloudflare.com; frame-src 'self' https://challenges.cloudflare.com; connect-src 'self' https://challenges.cloudflare.com;">
Common Patterns
Hono + Cloudflare Workers: Server-side validation in Workers API routes with Hono framework. Load
references/common-patterns.md #pattern-1 when building Workers endpoints requiring bot protection.
React + Next.js: Client-side forms with @marsidev/react-turnstile integration. Load
references/common-patterns.md #pattern-2 when integrating Turnstile with React/Next.js applications.
E2E Testing: Automated testing with dummy keys (Playwright, Cypress, Jest). Load
references/common-patterns.md #pattern-3 when writing E2E tests or setting up CI/CD pipelines.
Widget Lifecycle: Programmatic widget control for SPAs (render, reset, remove, getToken). Load
references/common-patterns.md #pattern-4 when building SPAs requiring explicit widget management.
When to Load References
: Configuring widget appearance, themes, execution modes, size, language, or retry behavior.references/widget-configs.md
: Debugging error codes 100*, 200*, 300*, 400*, 600* or troubleshooting client-side failures (CSP, domain errors, widget crashes).references/error-codes.md
: Setting up E2E tests (Playwright, Cypress), local development with dummy keys, or CI/CD pipeline integration.references/testing-guide.md
: Integrating with React, Next.js, or troubleshooting @marsidev/react-turnstile issues (Jest mocking, SSR, hooks).references/react-integration.md
: Building Hono Workers routes, React forms, E2E tests, or widget lifecycle management (explicit rendering).references/common-patterns.md
: Implementing pre-clearance for SPAs, custom actions/cdata, retry strategies, or multi-widget pages.references/advanced-topics.md
: Preparing for deployment, verifying complete setup, or ensuring production readiness (14-point checklist).references/setup-checklist.md
: Migrating from reCAPTCHA (v2) or hCaptcha to Turnstile, including compat mode, API differences, and POST-only Siteverify requirement.references/migration-guide.md
: Browser compatibility matrix, Safari 18 "Hide IP" workaround, Brave shields issues, and browser-specific fallbacks.references/browser-support.md
: WebView integration for iOS, Android, React Native, and Flutter, including User Agent consistency and storage persistence requirements.references/mobile-implementation.md
: wrangler-turnstile-config.jsonc (Workers env), turnstile-widget-implicit.html (static forms), turnstile-widget-explicit.ts (SPA rendering), turnstile-server-validation.ts (Siteverify API), turnstile-react-component.tsx (React integration), turnstile-hono-route.ts (Hono validation), turnstile-test-config.ts (testing setup)templates/
: Verify Content Security Policy allows Turnstile (usage: scripts/check-csp.sh
./scripts/check-csp.sh https://example.com)
Dependencies
Required: None (Turnstile loads from Cloudflare CDN)
Optional: @marsidev/react-turnstile@1.3.1 (React), turnstile-types@1.2.3 (TypeScript), vue-turnstile (Vue 3), ngx-turnstile (Angular), svelte-turnstile (Svelte), @nuxtjs/turnstile (Nuxt)
Official Documentation
Turnstile: https://developers.cloudflare.com/turnstile/ • Get Started: https://developers.cloudflare.com/turnstile/get-started/ • Error Codes: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes/ • Testing: https://developers.cloudflare.com/turnstile/troubleshooting/testing/ • Migration (reCAPTCHA): https://developers.cloudflare.com/turnstile/migration/recaptcha/ • MCP: Use
mcp__cloudflare-docs__search_cloudflare_documentation tool
Troubleshooting
Problem: Error 110200 - "Unknown domain"
Solution: Add your domain (including localhost for dev) to widget's allowed domains in Cloudflare Dashboard. For local dev, use dummy test sitekey
1x00000000000000000000AA instead.
Problem: Error 300030 - Widget crashes for legitimate users
Solution: Implement error callback with retry logic. This is a known Cloudflare-side issue (2025). Fallback to alternative verification if retries fail.
Problem: Tokens always return success: false
success: falseSolution:
- Check token hasn't expired (5 min TTL)
- Verify secret key is correct
- Ensure token hasn't been validated before (single-use)
- Check hostname matches widget configuration
Problem: CSP blocking iframe (Error 200500)
Solution: Add CSP directives:
<meta http-equiv="Content-Security-Policy" content=" frame-src https://challenges.cloudflare.com; script-src https://challenges.cloudflare.com; ">
Problem: Safari 18 "Hide IP" causing Error 300010
Solution: Document in error message that users should disable Safari's "Hide IP address" setting (Safari → Settings → Privacy → Hide IP address → Off)
Problem: Next.js + Jest tests failing with @marsidev/react-turnstile
Solution: Mock the Turnstile component in Jest setup:
// jest.setup.ts jest.mock('@marsidev/react-turnstile', () => ({ Turnstile: () => <div data-testid="turnstile-mock" />, }))
Token Efficiency: ~65-70% savings vs manual integration
Errors Prevented: 12 documented security/validation issues with complete solutions
Deployment Checklist: Load
references/setup-checklist.md for complete 14-point pre-deployment verification