| name | prompt-injection-denylist |
| description | Detect and investigate fraud rings that use Warp as a free LLM backend by injecting third-party system prompts (e.g., OpenClaw, Claude Code) into agent mode queries. Use when you see unusual agent mode cost spikes, high volumes of identical-looking first messages, or tool-call loops referencing tools that don't exist in Warp (e.g., Bash, Read, exec). This skill covers how to identify prompt injection patterns, backtest proposed denylist strings for false positives, and monitor cost impact.
|
Prompt Injection Denylist Analysis
Detect fraud groups that forward traffic from third-party AI tools through Warp's agent mode
by pasting full system prompts as their first message.
Schema config
Table/project/database names are logical placeholders ({{ fq.* }}) resolved from
config/schema.json (see SCHEMA.md). Run the SQL via
api_client.py query --database ugc --file <path> (or --sql) to resolve them, or
substitute physical names yourself. The default mapping uses project YOUR_GCP_PROJECT
and tables agent_mode_ugc, server_events, core_user_facts.
Background
How It Works
Fraudsters use Warp as a free LLM backend by:
- Creating throwaway accounts on disposable email domains (aol, yahoo, icloud, mail.ru, protonmail, hotmail, zohomail)
- Sending requests to Warp's agent mode where the "user query" is actually a full system prompt from another AI tool
- The LLM enters infinite loops calling tools that don't exist in Warp (e.g.,
Bash, Read, exec), burning credits
Known Fraud Groups (as of Feb 2026)
OpenClaw — An open-source AI assistant framework. Fraud pattern:
- First message is the full OpenClaw system prompt, including
AGENTS.md, SOUL.md, USER.md, HEARTBEAT.md, skill definitions, and tool instructions
- Contains distinctive opener:
"You are a personal assistant running inside OpenClaw"
- Includes
<available_skills> XML blocks and openclaw gateway CLI references
- Users build crypto/DeFi bots, stock trading assistants, digital twin systems
Claude Code Spoof — Users spoofing Anthropic's Claude Code CLI. Fraud pattern:
- Prefixes message with fake billing header:
x-anthropic-billing-header: cc_version=...; cc_entrypoint=cli; cch=00000;
- Pastes the full Claude Code system prompt after the header
- Distinctive opener:
"You are Claude Code, Anthropic's official CLI for Claude"
Shared injection wrapper (<language_requirement> ring) — Both groups (and others) wrap their prompts with a common Chinese-language injection framework:
<language_requirement> blocks specifying response language
<tool_instructions> blocks with Chinese batch-operation rules
- Contains a distinctive all-caps "batch operations" banner line
- Contains Chinese-language batch-read / tool-usage instruction phrases
Enforcement Mechanism
Denylist strings live in your server configuration (e.g. a user_query_denylist_strings list)
and are evaluated by the reputation/enforcement layer as a substring match against the user's
query. When a query matches and enforcement is enabled, the request is handled by the
server-side reputation system.
Keep the exact matching semantics, enforcement behavior, exemptions, and code locations in your
private server repository — do not document them in this open-source skill.
Detection Workflow
Step 1: Identify Suspicious Prompt Patterns
Look for high-volume identical first messages in the UGC table. Fraud prompts are long system
prompts, not natural user queries.
SELECT
LEFT(query, 200) AS query_prefix,
COUNT(*) AS occurrences,
COUNT(DISTINCT user_id) AS distinct_users,
ROUND(SUM(total_cost_dollars), 2) AS total_cost
FROM (
SELECT
user_id,
total_cost_dollars,
JSON_VALUE(request, '$.input.userInputs.inputs[0].userQuery.query') AS query
FROM {{ fq.agent_mode_ugc }}
WHERE event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
AND is_new_conversation = true
)
WHERE LENGTH(query) > 2000
GROUP BY query_prefix
HAVING occurrences > 10
ORDER BY total_cost DESC
Step 2: Extract Full Examples
Once you spot a pattern, pull full message examples to identify the exact system prompt being injected:
SELECT
user_id,
conversation_id,
email_domain,
days_since_signup,
LEFT(query, 5000) AS full_query
FROM (
SELECT
user_id,
conversation_id,
email_domain,
days_since_signup,
JSON_VALUE(request, '$.input.userInputs.inputs[0].userQuery.query') AS query
FROM {{ fq.agent_mode_ugc }}
WHERE event_date = CURRENT_DATE()
AND is_new_conversation = true
)
WHERE query LIKE '%<suspected_pattern>%'
LIMIT 4
Look for:
- Verbatim system prompt openers (e.g., "You are a personal assistant running inside...")
- XML/structured blocks (
<tool_instructions>, <available_skills>, <language_requirement>)
- Fake billing/auth headers
- References to tools that don't exist in Warp (Bash, Read, Write, exec)
- Chinese-language instruction blocks wrapping the system prompt
Step 3: Check JA4 TLS Fingerprints
Fraud rings often have distinct TLS fingerprints. Check via server events:
SELECT
json_extract_scalar(
to_json_string(event_properties),
'$.payload.fraud_signals.request_headers.X-JA4-Fingerprint'
) AS ja4,
COUNT(DISTINCT json_extract_scalar(to_json_string(event_properties), '$.payload.user_id')) AS distinct_users,
COUNT(*) AS requests
FROM {{ fq.server_events }}
WHERE event_name = 'agentmode.request'
AND event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
AND
GROUP BY ja4
ORDER BY distinct_users DESC
Note: Distributed fraud rings (like Claude Code Spoof) may have 90+ distinct JA4s, making JA4
blocking impractical. In that case, content-based denylist strings are the better approach.
Step 4: Propose Denylist Strings
Choose substrings that are:
- Unique to the injected system prompt — not something a legitimate user would type
- Exact system prompt openers — e.g., "You are a personal assistant running inside OpenClaw"
is safe, but "openclaw" alone would false-positive on users asking "how do I set up openclaw"
- Case-sensitive — the denylist uses case-sensitive substring matching
Good candidates:
- Exact system prompt opening sentences
- Fake billing/auth header prefixes
- XML block markers unique to the injection framework
- CLI reference strings embedded in the system prompt
Bad candidates (too broad):
- Single product names (e.g., "openclaw", "claude code") — legit users may mention these
- Common programming terms that appear in the system prompt
- Short strings that could appear in normal code
Step 5: Backtest for False Positives
Critical step. Before adding any denylist string, verify zero paid users would be caught.
Join on core_user_facts for reliable paid status (don't trust plan_type from UGC table):
SELECT
match_type,
COALESCE(cuf.active_subscription_plan_type, 'none') AS paid_status,
cuf.is_on_team_with_active_subscription,
COUNT(DISTINCT ugc.user_id) AS distinct_users,
COUNT(*) AS total_requests,
ROUND(SUM(ugc.total_cost_dollars), 2) AS total_cost
FROM (
SELECT
user_id,
total_cost_dollars,
CASE
WHEN query LIKE '%<candidate_string_1>%' THEN 'pattern_1'
WHEN query LIKE '%<candidate_string_2>%' THEN 'pattern_2'
END AS match_type
FROM (
SELECT
user_id,
total_cost_dollars,
JSON_VALUE(request, '$.input.userInputs.inputs[0].userQuery.query') AS query
FROM {{ fq.agent_mode_ugc }}
WHERE event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
)
WHERE query LIKE '%<candidate_string_1>%'
OR query LIKE '%<candidate_string_2>%'
) ugc
LEFT JOIN {{ fq.core_user_facts }} cuf ON ugc.user_id = cuf.user_id
GROUP BY match_type, paid_status, is_on_team_with_active_subscription
ORDER BY match_type, paid_status
Requirements to proceed:
- Zero paid users (
active_subscription_plan_type is not null/none)
- Zero users on a team with active subscription
- All matches come from disposable email domains and recent signups
Step 6: Monitor with Metabase
Create a day-by-day cost chart to track each fraud group's impact over time:
SELECT
event_date,
CASE
WHEN query LIKE '%<denylist_string_1>%' THEN 'Group 1'
WHEN query LIKE '%<denylist_string_2>%' THEN 'Group 2'
END AS fraud_group,
ROUND(SUM(total_cost_dollars), 2) AS daily_cost,
COUNT(*) AS requests,
COUNT(DISTINCT user_id) AS distinct_users
FROM (
SELECT
user_id,
event_date,
total_cost_dollars,
JSON_VALUE(request, '$.input.userInputs.inputs[0].userQuery.query') AS query
FROM {{ fq.agent_mode_ugc }}
WHERE event_date BETWEEN {{start_date}} AND {{end_date}}
)
WHERE <denylist conditions>
GROUP BY event_date, fraud_group
ORDER BY event_date, fraud_group
Use Metabase template tags ({{start_date}}, {{end_date}}) for date parameters.
Display as line chart, X-axis = event_date, Y-axis = daily_cost, series = fraud_group.
Create a Metabase question in your fraud collection for ongoing monitoring.
Managing Denylist Strings
The live denylist strings are maintained in your private server configuration and are
intentionally not listed here: publishing exact strings would let abusers trivially edit
their prompts to evade them. Record new strings in your internal config/change log rather than
in this open-source skill.
When you add a string, backtest it over a recent window and confirm zero paid users are
affected before enabling it (see Step 5).
Important Notes
- Denylist is a content-based approach — it catches the prompt regardless of IP, device, or JA4 fingerprint. This is important because distributed fraud rings rotate all of those signals.
- Enforcement is handled server-side — how matched requests are treated, and any exemptions (e.g. paying customers), live in your private server code. Keep those specifics out of this public doc.
- New fraud tools will appear — When you see a new cost spike, check if the queries contain a new third-party system prompt being injected. Follow this same workflow to identify, backtest, and add new denylist strings.
- Confirm match semantics — Check how your denylist matches (e.g. case sensitivity) so proposed strings match the fraud messages exactly.