cloudflare-turnstile
Install this skill
npx skills add jezweb/claude-skillsWorks across Claude Code, Cursor, Codex, Copilot & Antigravity
Cloudflare Turnstile is a non-intrusive challenge-response security mechanism that replaces traditional CAPTCHAs by verifying human behavior without requiring user interaction. It functions by embedding a lightweight script that validates requests against the Cloudflare edge network. This skill facilitates the integration of Turnstile widgets into web applications, ensuring all tokens are validated server-side to prevent bot traffic, spam, or malicious automated form submissions. It specifically handles the lifecycle of tokensβfrom rendering the frontend widget to executing secure POST requests against the /siteverify endpoint. By managing both implicit and explicit rendering modes, this skill enables developers to secure sensitive entry points like login screens and contact forms while maintaining strict compliance with Cloudflare's mandatory server-side verification requirements.
When to Use This Skill
- β’Protecting user registration and authentication forms from credential stuffing
- β’Securing API endpoints against automated scrapers and brute-force attacks
- β’Reducing spam submissions on contact and newsletter sign-up forms
- β’Validating human traffic on high-risk checkout pages
How to Invoke This Skill
Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:
- βset up Cloudflare Turnstile on my contact form
- βhow do I validate a Turnstile token server side
- βadd anti-bot protection to my React application
- βhow to implement Cloudflare Turnstile with siteverify
- βfix Cloudflare Turnstile integration issues
Pro Tips
- π‘Always pass the `cf-turnstile-response` token from the frontend to your backend and validate it with Cloudflare's API. Never trust frontend validation alone.
- π‘Leverage the `@marsidev/react-turnstile` library for React applications to simplify integration and manage component lifecycle effectively, especially concerning callback changes.
- π‘Monitor Cloudflare Turnstile analytics regularly to gain insights into traffic patterns, bot behavior, and the effectiveness of your challenge configurations.
What this skill does
- β’Automatic verification of human behavior through browser telemetry
- β’Server-side token validation via the Siteverify API
- β’Support for both implicit and explicit widget rendering cycles
- β’Integration for React applications using the @marsidev library
- β’Support for dummy sitekeys for development and testing environments
When not to use it
- βLow-traffic static sites where bot activity is not a threat
- βApplications requiring complete offline or private intranet functionality without external CDN access
Example workflow
- Register a domain on the Cloudflare dashboard to receive a sitekey and secret key
- Embed the Turnstile JavaScript client in your HTML page to generate the widget
- Capture the cf-turnstile-response token upon user form submission
- Send the token and the client IP address to the /siteverify endpoint via a server-side POST request
- Verify the success boolean in the JSON response before proceeding with form logic
Prerequisites
- βActive Cloudflare account
- βRegistered domain or hostname in the Turnflare dashboard
- βBackend environment capable of making HTTPS POST requests
Pitfalls & limitations
- !Tokens are strictly single-use and expire within five minutes
- !Failure to include the remoteip in the verification request can lead to failed validations
- !Relying on client-side success callbacks without server-side verification is a critical security vulnerability
FAQ
How it compares
Unlike manual CAPTCHA implementation or generic regex filters, this skill enforces the mandatory siteverify handshake, ensuring that tokens cannot be forged or reused by bots.
π Full skill instructions β original source: jezweb/claude-skills
**Status**: Production Ready β
**Last Updated**: 2026-01-21
**Dependencies**: None (optional: @marsidev/react-turnstile for React)
**Latest Versions**: @marsidev/[email protected], [email protected]
**Recent Updates (2025)**:
- **December 2025**: @marsidev/react-turnstile v1.4.1 fixes race condition in script loading
- **August 2025**: v1.3.0 adds
rerenderOnCallbackChange prop for React closure issues- **March 2025**: Upgraded Turnstile Analytics with TopN statistics (7 dimensions: hostnames, browsers, countries, user agents, ASNs, OS, source IPs), anomaly detection, enhanced bot behavior monitoring
- **January 2025**: Brief remoteip validation enforcement (resolved, but highlights importance of correct IP passing)
- **2025**: WCAG 2.1 AA compliance, Free plan (20 widgets, 7-day analytics), Enterprise features (unlimited widgets, ephemeral IDs, any hostname support, 30-day analytics, offlabel branding)
---
## Quick Start (5 Minutes)
# 1. Create widget: https://dash.cloudflare.com/?to=/:account/turnstile
# Copy sitekey (public) and secret key (private)
# 2. Add widget to frontend
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Submit</button>
</form>
# 3. Validate token server-side (Cloudflare Workers)
const formData = await request.formData()
const token = formData.get('cf-turnstile-response')
const verifyFormData = new FormData()
verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY)
verifyFormData.append('response', token)
verifyFormData.append('remoteip', request.headers.get('CF-Connecting-IP')) // REQUIRED - see Critical Rules
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', { status: 401 })**CRITICAL:**
- Token expires in 5 minutes, single-use only
- ALWAYS validate server-side (Siteverify API required)
- Never proxy/cache api.js (must load from Cloudflare CDN)
- Use different widgets for dev/staging/production
## Rendering Modes
**Implicit** (auto-render on page load):
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-callback="onSuccess"></div>**Explicit** (programmatic control for SPAs):
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
const widgetId = turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY' })
turnstile.reset(widgetId) // Reset widget
turnstile.getResponse(widgetId) // Get token**React** (using @marsidev/react-turnstile):
import { Turnstile } from '@marsidev/react-turnstile'
<Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} />---
## 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
β **Always pass client IP to Siteverify** - Use
CF-Connecting-IP header (Workers) or X-Forwarded-For (Node.js). Cloudflare briefly enforced strict remoteip validation in Jan 2025, causing widespread failures for sites not passing correct IP### 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 **15** 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. Turnstile tokens are single-use - after validation (success OR failure), the token is consumed and cannot be revalidated. Developers must explicitly call
turnstile.reset() to generate a new token for subsequent submissions.**Prevention**: Templates document single-use constraint and token refresh patterns
// CRITICAL: Reset widget after validation to get new token
const turnstileRef = useRef(null)
async function handleSubmit(e) {
e.preventDefault()
const token = formData.get('cf-turnstile-response')
const result = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ token })
})
// Reset widget regardless of success/failure
// Token is consumed either way
if (turnstileRef.current) {
turnstile.reset(turnstileRef.current)
}
}
<Turnstile
ref={turnstileRef}
siteKey={TURNSTILE_SITE_KEY}
onSuccess={setToken}
/>### Issue #13: Error 106010 - Chrome/Edge First-Load Failure
**Error**:
106010 - "Generic parameter error" on first widget load in Chrome/Edge browsers**Source**: [Cloudflare Error Codes](https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes/), [Community Report](https://community.cloudflare.com/t/turnstile-inconsistent-errors/856678)
**Why It Happens**: Unknown browser-specific issue affecting Chrome and Edge on first page load. Console shows 400 error to
https://challenges.cloudflare.com/cdn-cgi/challenge-platform. Firefox is not affected. Subsequent page reloads work correctly.**Prevention**: Implement error callback with auto-retry logic
turnstile.render('#container', {
sitekey: SITE_KEY,
retry: 'auto',
'retry-interval': 8000,
'error-callback': (errorCode) => {
if (errorCode === '106010') {
console.warn('Chrome/Edge first-load issue (106010), auto-retrying...')
// Auto-retry will handle it
}
}
})**Workaround**: Widget works correctly after page reload. Auto-retry setting resolves in most cases. Test in Incognito mode to rule out browser extensions. Review CSP rules to ensure Cloudflare Turnstile endpoints are allowed.
### Issue #14: Multiple Widgets Visual Status Stuck (Community-sourced)
**Error**: Widget displays "Pending..." status even after successful token generation
**Source**: [GitHub Issue #119](https://github.com/marsidev/react-turnstile/issues/119)
**Why It Happens**: CSS repaint issue when rendering multiple
<Turnstile/> components on a single page. Only reproducible on full HD desktop screens. Token IS successfully generated (validation works), but visual status doesn't update. Hovering over widget triggers repaint and shows correct status.**Prevention**: Force CSS repaint in success callback
<Turnstile
siteKey={KEY}
onSuccess={(token) => {
setToken(token)
// Force repaint by toggling display
const widget = document.querySelector('.cf-turnstile')
if (widget) {
widget.style.display = 'none'
setTimeout(() => widget.style.display = 'block', 0)
}
}}
/>**Note**: This is a visual-only issue, not a validation failure. The token is correctly generated and functional.
### Issue #15: Jest Compatibility with @marsidev/react-turnstile (Updated Dec 2025)
**Error**:
Jest encountered an unexpected token when importing @marsidev/react-turnstile**Source**: [GitHub Issue #114](https://github.com/marsidev/react-turnstile/issues/114), [GitHub Issue #112](https://github.com/marsidev/react-turnstile/issues/112)
**Why It Happens**: ESM module resolution issues with Jest 30.2.0 (latest as of Dec 2025). Issue #112 closed as "not planned" by maintainer. Jest users are stuck; Vitest migration works.
**Prevention**: Mock the Turnstile component in Jest setup OR migrate to Vitest
// Option 1: Jest mocking (jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
Turnstile: () => <div data-testid="turnstile-mock" />,
}))
// Option 2: transformIgnorePatterns in jest.config.js
module.exports = {
transformIgnorePatterns: [
'node_modules/(?!(@marsidev/react-turnstile)/)'
]
}
// Option 3 (Recommended): Migrate to Vitest
// Vitest handles ESM modules correctly without mocking**Status**: Maintainer closed issue as "not planned". Recommend migrating to Vitest for new projects.
## Configuration
**wrangler.jsonc:**
{
"vars": { "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" },
"secrets": ["TURNSTILE_SECRET_KEY"] // Run: wrangler secret put TURNSTILE_SECRET_KEY
}**Required CSP:**
<meta http-equiv="Content-Security-Policy" content="
script-src 'self' https://challenges.cloudflare.com;
frame-src 'self' https://challenges.cloudflare.com;
">---
## 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<{ Bindings: Bindings }>()
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') || '') // CRITICAL - always pass client IP
const verifyResult = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
method: 'POST',
body: verifyFormData,
}
)
const outcome = await verifyResult.json<{ success: boolean }>()
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<string>()
const [error, setError] = useState<string>()
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
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 (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<textarea name="message" required />
<Turnstile
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
onSuccess={setToken}
onError={() => setError('Challenge failed')}
onExpire={() => setToken(undefined)}
/>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={!token}>
Submit
</button>
</form>
)
}**When to use**: Client-side forms in Next.js with React hooks
---
## Testing Keys
**Dummy Sitekeys (client):**
- Always pass:
1x00000000000000000000AA- Always block:
2x00000000000000000000AB- Force interactive:
3x00000000000000000000FF**Dummy Secret Keys (server):**
- Always pass:
1x0000000000000000000000000000000AA- Always fail:
2x0000000000000000000000000000000AA- Token already spent:
3x0000000000000000000000000000000AA---
## Bundled Resources
**Scripts:**
check-csp.sh - Verify CSP allows Turnstile**References:**
-
widget-configs.md - All configuration options-
error-codes.md - Error code troubleshooting (100*/200*/300*/400*/600*)-
testing-guide.md - Testing strategies, dummy keys-
react-integration.md - React/Next.js patterns**Templates:** Complete examples for Hono, React, implicit/explicit rendering, validation
---
## Advanced Features
**Pre-Clearance (SPAs):** Issue cookie that persists across page navigations
turnstile.render('#container', {
sitekey: SITE_KEY,
callback: async (token) => {
await fetch('/api/pre-clearance', { method: 'POST', body: JSON.stringify({ token }) })
}
})**Custom Actions & Data:** Track challenge types, pass custom data (max 255 chars)
turnstile.render('#container', {
action: 'login', // Track in analytics
cdata: JSON.stringify({ userId: '123' }), // Custom payload
})**Error Handling:** Use
retry: 'auto' and error-callback for resilienceturnstile.render('#container', {
retry: 'auto',
'retry-interval': 8000, // ms between retries
'error-callback': (error) => { /* handle or show fallback */ }
})---
## Dependencies
**Required:** None (loads from CDN)
**React:** @marsidev/[email protected] (Cloudflare-recommended), [email protected]
**Other:** vue-turnstile, ngx-turnstile, svelte-turnstile, @nuxtjs/turnstile
---
## Official Documentation
- https://developers.cloudflare.com/turnstile/
- 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**Solution**:
1. Check token hasn't expired (5 min TTL)
2. Verify secret key is correct
3. Ensure token hasn't been validated before (single-use)
4. 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 (or migrate to Vitest, which handles ESM modules correctly):
// Option 1: Jest mocking (jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
Turnstile: () => <div data-testid="turnstile-mock" />,
}))
// Option 2: Migrate to Vitest (recommended for new projects)
// Vitest handles ESM modules without mocking required**Note**: This issue persists in Jest 30.2.0 (Dec 2025). Maintainer closed as "not planned". See Issue #15 for full details.
---
**Errors Prevented**: 15 documented issues (Safari 18 Hide IP, Brave confetti, Next.js Jest, CSP blocking, token reuse, expiration, hostname allowlist, widget crash 300030, config error 600010, missing validation, GET request, secret exposure, Chrome/Edge 106010, multiple widgets rendering, token regeneration pattern)
---
---
paths: "**/*.ts", "**/*.tsx", "**/*turnstile*.ts"
---
# Cloudflare Turnstile Corrections
## Server-Side Validation MANDATORY
/* β SECURITY VULNERABILITY - client-only validation */
// Frontend gets token, sends to your API
// API trusts token without verification
/* β
ALWAYS call Siteverify API */
async function verifyTurnstile(token: string, env: Env): Promise<boolean> {
const response = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
secret: env.TURNSTILE_SECRET_KEY,
response: token,
}),
}
)
const result = await response.json()
return result.success === true
}## Siteverify: POST Only, Not GET
/* β GET not supported (unlike reCAPTCHA) */
fetch(https://challenges.cloudflare.com/turnstile/v0/siteverify?secret=...&response=...)
/* β
Must use POST */
fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
body: new URLSearchParams({ secret, response: token }),
})## Token Expires in 5 Minutes
/* β οΈ Tokens have 300 second TTL */
// User completes challenge
// Token valid for 5 minutes only
// After that, siteverify returns success: false
/* β
Verify immediately after form submission */## Never Expose Secret Key
/* β Secret in frontend */
const TURNSTILE_SECRET = 'xxx' // In client code!
/* β
Secret only in backend environment */
// .dev.vars or wrangler secret
// Access via env.TURNSTILE_SECRET_KEY## Test Keys for Development
/* β
Dummy keys for testing */
// Site key (always passes): 1x00000000000000000000AA
// Site key (always fails): 2x00000000000000000000AB
// Site key (forces challenge): 3x00000000000000000000FF
// Secret key (always passes): 1x0000000000000000000000000000000AA
// Secret key (always fails): 2x0000000000000000000000000000000AB## CSP Configuration Required
/* β
Allow Turnstile iframe in CSP */
// frame-src: https://challenges.cloudflare.com
// script-src: https://challenges.cloudflare.com## Quick Fixes
| If Claude suggests... | Use instead... |
|----------------------|----------------|
| Client-only validation | ALWAYS call Siteverify API server-side |
| GET request to Siteverify | POST with URLSearchParams body |
| Secret key in frontend | Keep in backend env only |
| Production keys in dev | Use dummy test keys |
| Missing CSP | Allow challenges.cloudflare.com |
How to Use This Skill Unit
Option A: Project-Specific (Recommended)
- Click "Download" above
- In your project, create the directory:
.agent/skills/cloudflare-turnstile/ - Save the file as
SKILL.md - The agent will automatically discover the skill based on its description.
Option B: Global Installation (All Agents)
Save the file to these locations to make it available across all projects:
- Claude Code:
~/.claude/skills/jezweb/claude-skills/cloudflare-turnstile/SKILL.md - Cursor:
~/.cursor/skills/jezweb/claude-skills/cloudflare-turnstile/SKILL.md - Antigravity:
~/.gemini/antigravity/skills/jezweb/claude-skills/cloudflare-turnstile/SKILL.md
π Install with CLI:npx skills add jezweb/claude-skills
