fetch#
Security tooling for LLM web fetches. Validates URLs, sanitizes responses, detects prompt injection and data exfiltration, and optionally escalates to an LLM verdict for confirmed-malicious blocking.
What it does#
- URL validation — blocks private IPs, localhost, cloud metadata endpoints, and non-HTTPS schemes
- Response sanitization — strips HTML tags, detects injection patterns, neutralizes matched payloads
- Exfiltration detection — flags external beacons (img, script, iframe, CSS url, markdown images)
- Two-signal enforcement — heuristic regex detection + optional LLM verdict
- Heuristic hit → neutralize (replace matched patterns with
[REDACTED]) - LLM verdict "malicious" → block (replace entire response with
BLOCKED: prompt injection detected.)
- Heuristic hit → neutralize (replace matched patterns with
- Rate limiting — per-session request budget to prevent runaway fetching
- Audit logging — JSONL log of all requests, responses, flags, and actions
Installation#
vlt add --ignore-scripts @usrrname/badger-fetch
# or: npm install --ignore-scripts @usrrname/badger-fetch
Requires Node ≥ 22.6. The install is lean — the host-specific SDKs
(@modelcontextprotocol/sdk for Claude, @opencode-ai/plugin for OpenCode) are
optional peer dependencies and are not pulled in by default. --ignore-scripts
is safe here: every badger package ships zero lifecycle scripts (see
Supply-chain posture).
Setup#
Run the locally-installed setup bin, pick your host and scope, and badger
writes the matching config (and installs the one SDK that host needs). Nothing is
written on npm install — setup is always an explicit, opt-in step.
./node_modules/.bin/badger-fetch-init # interactive: prompts for host + scope
./node_modules/.bin/badger-fetch-init --claude # MCP server + Pre/PostToolUse hooks (installs the MCP SDK)
./node_modules/.bin/badger-fetch-init --opencode # OpenCode plugin entry
Run it from node_modules/.bin (or pnpm exec badger-fetch-init /
npm exec --no-install -- badger-fetch-init) rather than npx @usrrname/... —
that executes the version you installed and vetted, never one fetched-and-run
from the registry on the spot.
| Flag | Effect |
|---|---|
--claude / --opencode |
choose the host (prompted if omitted) |
--project / --global |
write into the repo (default) or your user config |
--no-install |
write config only; skip installing the MCP SDK |
-y, --yes |
skip the confirmation prompt |
What it writes:
| Host | Files | Contents |
|---|---|---|
| Claude | .mcp.json, .claude/settings.json |
MCP server badger-fetch + WebFetch|WebSearch Pre/PostToolUse hooks |
| OpenCode | opencode.json |
@usrrname/badger-fetch/opencode plugin entry |
--global targets ~/.claude.json + ~/.claude/settings.json (Claude) or
~/.config/opencode/opencode.json (OpenCode). Re-running is idempotent — it
merges into existing config without clobbering other servers, hooks, or plugins.
Usage#
MCP Server#
badger-fetch-init --claude registers this for you. To wire it by hand instead,
add to your MCP client config (e.g. .mcp.json for Claude Code):
{
"mcpServers": {
"badger-fetch": {
"command": "node",
"args": ["node_modules/@usrrname/badger-fetch/dist/index.js"]
}
}
}
The MCP server needs the optional peer @modelcontextprotocol/sdk — init
installs it pinned; if wiring by hand, run
vlt add --ignore-scripts @modelcontextprotocol/sdk@1.29.0 too.
Add "deny": ["WebFetch"] to force all fetching through the fetch tool.
Tools exposed:
| Tool | Purpose |
|---|---|
fetch |
Fetch a URL with security validation |
audit |
Review the audit log |
fetch_approve |
Approve a previously blocked URL |
approve_llm_analysis |
Approve LLM analysis for an injection-flagged URL |
Claude Code Hooks#
node packages/fetch/src/claude/pre-tool-use.ts # rate limiting
node packages/fetch/src/claude/post-tool-use.ts # injection scan
Configure via badger-hook-config.json or env vars.
Note: Claude hooks run after the tool result is in context, so they can only warn — not block. Use the MCP server or OpenCode plugin for blocking capability.
WebFetch & WebSearch results#
Both hooks (Claude PostToolUse, OpenCode after-hook) scan WebFetch and WebSearch results. WebFetch returns text, scanned directly. WebSearch returns a structured results object; parseWebSearch normalizes it to per-result text — collecting every result string at any depth, across results/content — so the detectors see clean per-result text instead of a JSON.stringify blob (whose ","url":" punctuation would split field-boundary payloads). Unknown shapes fall back to the raw JSON blob, so they are never left unscanned. Same detect/neutralize core for both tools — WebSearch is at parity with WebFetch, not crudely flattened.
Out of scope: per-result exfil-URL parsing (deferred). Outbound exfiltration is covered separately by the planted-canary block.
OpenCode Plugin#
badger-fetch-init --opencode adds this for you. To wire it by hand, add to
opencode.json:
{
"plugin": [
["@usrrname/badger-fetch/opencode", {
"maxWebRequestsPerSession": 20,
"auditLogPath": ".badger/findings.jsonl",
"llmApiKey": "...",
"llmModel": "nvidia/nemotron-3.5-content-safety:free",
"llmApiUrl": "https://openrouter.ai/api/v1",
"llmAnalysisThreshold": "flagged"
}]
]
}
Plugin options override environment variables.
Supply-chain posture#
badger-fetch is a security tool and is packaged like one:
- No on-demand
npxexecution. Install the package, then run the bin fromnode_modules/.bin(orpnpm exec/npm exec --no-install). You execute the version you installed and vetted — never one resolved-and-run from the registry on the spot. - No lifecycle scripts. This package (and the rest of the workspace) ships no
preinstall/install/postinstall/preparescripts, sonpm installruns no code.--ignore-scriptskeeps that property across the dependency tree. - Pinned, scripts-disabled SDK install.
init --claudeinstalls the MCP SDK as<pm> add --ignore-scripts @modelcontextprotocol/sdk@<pinned>— the exact version declared in this package'speerDependencies, with lifecycle scripts off. If the package manager rejects--ignore-scripts,initaborts and prints the command instead of falling back to a scripts-enabled install. - Nothing is written on install. Setup is an explicit
badger-fetch-initstep; installing the package never edits~/.claude,.mcp.json, oropencode.json.
Configuration#
| Variable | Default | Description |
|---|---|---|
OPENROUTER_API_KEY |
— | Enables LLM-based injection analysis (optional) |
BADGER_CONFIG |
— | Path to optional JSON config file |
BADGER_LOG_DIR |
/tmp |
Directory for the audit log |
Config file options (JSON):
| Option | Default | Description |
|---|---|---|
maxResponseBytes |
524288 | Max response size before truncation |
allowedSchemes |
["https"] |
Allowed URL schemes |
domainAllowlist |
[] |
Explicitly allowed domains |
domainDenylist |
[] |
Explicitly denied domains |
rateLimitMax |
30 | Max requests per window |
rateLimitWindowMs |
60000 | Rate limit window |
llmModel |
nvidia/nemotron-3.5-content-safety:free |
LLM model for analysis |
llmAnalysisThreshold |
"flagged" |
When to invoke LLM (never, flagged, always) |
Enforcement Policy#
Two-signal pass/neutralize/block:
| Heuristic hit? | LLM verdict | Action |
|---|---|---|
| No | (not called) | pass |
| Yes | null (no key) |
neutralize |
| Yes | "safe" |
neutralize |
| Yes | "suspicious" |
neutralize |
| Yes | "malicious" |
block |
- Neutralize — replaces matched injection patterns with
[REDACTED] - Block — replaces entire response with
BLOCKED: prompt injection detected. - Blocked content is preserved in the audit log as a base64-encoded JSON envelope for forensics
Detection Capabilities#
Injection detection — 17 rules across 4 categories:
- Direct instruction overrides ("ignore previous instructions")
- Tool/system spoofing (
<system-reminder>, fake tool turns) - Social engineering (role claims, mode overrides, prompt extraction)
- Malicious action directives ("make repos public", "exfiltrate results")
Obfuscation detection — catches evasion through:
- Zero-width characters
- Homoglyphs (Cyrillic/Greek confusables)
- Character spacing
- Base64/hex encoding
- Typoglycemia (scrambled middle letters)
Exfiltration detection — flags external beacons:
<img>,<script>,<iframe>,<link>,<object>,<embed>,<video>,<audio>- CSS
url()in inline styles - Markdown image syntax

Canary detection (canary_leak)#
badger plants an opaque, prefix-free honeytoken per session and watches for it to surface:
- Planting — Claude adds the token via
SessionStartadditionalContext; OpenCode injects it viaexperimental.chat.system.transform. The bare token is planted with no "secret, never reveal" wrapper, so an attacker cannot pattern-strip it. - Detection — the token appearing in assistant output triggers
canary_leak(extraction). The token appearing in any outbound tool arg triggerscanary_leakand the call is blocked (exfiltration). Cross-session exfil is caught: any active session's token in another session's outbound args is flagged. - Minting rule — one token per new planting. Claude mints on
startup/clearand reuses onresume/compact; OpenCode mints on first sight of a newsessionID. - Limitation — badger cannot plant into Anthropic's real system prompt;
canary_leakdetects that badger's own planted marker leaked, not that the platform prompt leaked.
Development#
pnpm -F fetch build # compiles to dist/
pnpm -F fetch test # unit tests (vitest)
License#
MIT