Claude Code mod: blocks running scripts straight from the internet (download-and-execute pipes, encoded PowerShell, LOLBin downloads)

A Claude Code mod (v2.1.287+) that stops Claude from running scripts straight from the internet.
Mod لـ Claude Code يمنع Claude من تشغيل أي سكربت يُنزَّل من الإنترنت ويُنفَّذ مباشرة — أسلوب شائع في نشر البرمجيات الخبيثة. يوقف الأمر قبل التنفيذ، ويوجّه Claude إلى الطريقة الآمنة: نزّل الملف، اعرضه على المستخدم، ولا تشغّله إلا بموافقته.
A rule in CLAUDE.md is advice: the user can talk Claude out of it, and a malicious README or web page can try to. A mod runs inside Claude Code itself — the command never reaches the shell, whatever the conversation says.
In our test, Claude first refused because of a CLAUDE.md rule. After an explicit "I approve, run it", it tried — and script-gate blocked it. Claude then switched on its own to downloading the file without running it.
| Pattern | Example | |||||
|---|---|---|---|---|---|---|
| Download piped into a shell/interpreter (through any number of pipeline stages) | `curl … \ | sh, curl … \ | tee f \ | bash, wget -qO- … \ | bash, iwr … \ | iex` |
| Download to a file, then run that file in the same command | curl -o i.sh … && bash i.sh, …; chmod +x i.sh; ./i.sh | |||||
| Download inside command/process substitution | bash -c "$(curl …)", bash <(curl …), `curl … \ | tee >(bash)` | ||||
PowerShell iex on remote content | iex (New-Object Net.WebClient).DownloadString(…) | |||||
| Encoded PowerShell | powershell -EncodedCommand … | |||||
| Windows LOLBins used as downloaders | certutil -urlcache, bitsadmin /transfer, mshta http…, regsvr32 /i:http… | |||||
| Python executing downloaded code | exec(urlopen(…).read()) |
Allowed: curl -o file (then inspecting it with cat/sha256sum), curl … | jq, iwr … -OutFile, git clone, npm install, running a local ./install.sh in a separate command.
Also allowed since v0.2 — interpreters that only read the download as data: curl … | python3 -m json.tool, and inline programs such as python3 -c '…', node -e '…', perl -ne '…', as long as the inline code has nothing that could turn the input back into code (exec, eval, compile, system, subprocess, child_process, pickle, …). Shells and PowerShell are never treated as data consumers, even with -c.
On a block it shows a 🧱 toast and a counter in the status line.
/plugin marketplace add ABDUAZIZX/script-gate
/plugin install script-gate@script-gate
Or for one session:
git clone https://github.com/ABDUAZIZX/script-gate
claude --plugin-dir ./script-gate
claude plugin validate .
claude plugin test .
Test samples are assembled from pieces so antivirus scanners don't flag the test file itself (Windows Defender killed a shell command containing them during development — they are real attack patterns).
!.Also see env-guard — blocks Claude from reading .env secrets.
| tee f | sh), >(bash), and download-then-run in one command; stops blocking data-only consumers such as | python3 -m json.tool (found while installing on Linux, where piping a local API into python3 -m json.tool is routine).MIT License
hooks/register.ts 106 lines1import type { EngineInterface, Register } from 'claude-code'
2
3const FETCH = String.raw`(?:curl|wget|iwr|irm|invoke-webrequest|invoke-restmethod)`
4const RUN = String.raw`(?:sh|bash|zsh|dash|ksh|fish|python3?|perl|ruby|node|php|iex|invoke-expression|pwsh|powershell)(?:\.exe)?`
5
6const FETCH_RE = new RegExp(String.raw`\b${FETCH}\b`, 'i')
7// A pipeline stage that starts an interpreter, optionally behind sudo/env/command/xargs.
8const STAGE_RE = new RegExp(
9 String.raw`^[\s({&]*(?:(?:sudo|env|command|exec|xargs)\s+(?:-\S+\s+)*)*(${RUN})\b(.*)$`,
10 'is',
11)
12// Shells and PowerShell execute whatever they read — never a data-only consumer.
13const SHELL_RE = /^(?:sh|bash|zsh|dash|ksh|fish|iex|invoke-expression|pwsh|powershell)(?:\.exe)?$/i
14// Inline-code flags: python -c, node -e/-p, perl/ruby -e/-ne/-pe, php -r. With one of these,
15// stdin is data for the inline program, not code — unless that program executes it.
16const INLINE_FLAG_RE = /^\s+-[a-z]*[cepr]\s/i
17// Anything that could turn stdin back into code. Heuristic, so it errs towards blocking.
18const EXECUTES_RE =
19 /\b(?:exec|eval|compile|system|popen|spawn|execSync|spawnSync|instance_eval|class_eval|__import__|importlib|subprocess|child_process|pickle|marshal|dill|vm|Function|qx)\b|`/i
20
21function isDataConsumer(interpreter: string, args: string, command: string) {
22 if (SHELL_RE.test(interpreter)) return false
23 if (/^python3?$/i.test(interpreter) && /^\s+-m\s+json\.tool\b/.test(args)) return true
24 return INLINE_FLAG_RE.test(args) && !EXECUTES_RE.test(command)
25}
26
27// Downloaded output piped into an interpreter, through any number of pipeline stages
28// (so `curl … | tee f | sh` is caught too).
29export function pipesIntoInterpreter(command: string) {
30 const stages = command.replace(/\|\|/g, '\n').split('|')
31 const first = stages.findIndex(s => FETCH_RE.test(s))
32 if (first === -1) return false
33 return stages.slice(first + 1).some(stage => {
34 const m = STAGE_RE.exec(stage)
35 return m !== null && !isDataConsumer(m[1], m[2], command)
36 })
37}
38
39const OUTPUT_RE = new RegExp(
40 String.raw`\b${FETCH}\b[^;&|\n]*?(?:\s(?:-[A-Za-z]*[oO]|--output(?:-document)?|-OutFile)(?:\s+|=)|\s*>\s*)["']?([^\s"';&|<>)]+)`,
41 'gi',
42)
43const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
44
45// Downloaded to a file, then that same file is run in the same command:
46// `curl -o i.sh URL && bash i.sh`, `… ; chmod +x i.sh; ./i.sh`, `… && . ./i.sh`.
47export function downloadsThenRuns(command: string) {
48 for (const m of command.matchAll(OUTPUT_RE)) {
49 const file = m[1].replace(/^\.[\/\\]/, '')
50 if (file === '-' || /^https?:/i.test(file)) continue
51 const rest = command.slice((m.index ?? 0) + m[0].length)
52 const f = escapeRe(file)
53 const end = String.raw`(?=$|[\s"';&|)])`
54 const ways = [
55 // bash i.sh, python3 -u i.py, source i.sh
56 String.raw`\b(?:${RUN}|source)\s+(?:-\S+\s+)*["']?(?:\.[\/\\])?${f}${end}`,
57 // . ./i.sh (dot-source)
58 String.raw`(?:^|[\s;&|(])\.\s+["']?(?:\.[\/\\])?${f}${end}`,
59 // ./i.sh or .\i.ps1 in command position
60 String.raw`(?:^|[;&|(])\s*\.[\/\\]${f}${end}`,
61 // /tmp/i.sh in command position, when the download target was a path
62 ...(file.includes('/') || file.includes('\\') ? [String.raw`(?:^|[;&|(])\s*["']?${f}${end}`] : []),
63 ]
64 if (ways.some(w => new RegExp(w, 'i').test(rest))) return true
65 }
66 return false
67}
68
69// Each rule: why it is dangerous, and a check that recognises it.
70const RULES: ReadonlyArray<readonly [string, (command: string) => boolean]> = [
71 ['تنزيل وتشغيل مباشر (download | shell)', pipesIntoInterpreter],
72 ['تنزيل ملف ثم تشغيله في الأمر نفسه', downloadsThenRuns],
73 ['تشغيل ناتج تنزيل عبر $( ) أو <( ) أو >( )', c => new RegExp(String.raw`(?:\$\(|<\(|\x60)\s*${FETCH}\b|\b${FETCH}\b[^;&\n]*>\(\s*${RUN}\b`, 'i').test(c)],
74 ['PowerShell: iex على محتوى من الإنترنت', c => new RegExp(String.raw`\b(?:iex|invoke-expression)\b[\s(]*(?:\(?\s*new-object\s+(?:system\.)?net\.webclient\)?\s*\.\s*download(?:string|data)|${FETCH}\b)`, 'i').test(c)],
75 ['PowerShell بأمر مشفّر (-EncodedCommand)', c => /\b(?:powershell|pwsh)(?:\.exe)?\b.*\s-(?:e|ec|enc|encodedcommand)\s+[A-Za-z0-9+/=]{16,}/i.test(c)],
76 ['أداة نظام تُستغل للتنزيل (certutil/bitsadmin/mshta/regsvr32)', c => /\b(?:certutil(?:\.exe)?\b.*-urlcache|bitsadmin(?:\.exe)?\s+\/transfer|mshta(?:\.exe)?\s+["']?(?:https?|javascript|vbscript):|regsvr32(?:\.exe)?\b.*\/i:\s*https?:)/i.test(c)],
77 ['Python ينفّذ كوداً منزّلاً', c => /\bexec\s*\(.*\b(?:urlopen|requests\.get|urllib)/i.test(c)],
78]
79
80let blocked = 0
81
82export function verdict(command: string): string | undefined {
83 for (const [why, matches] of RULES) if (matches(command)) return why
84 return undefined
85}
86
87function deny($: EngineInterface, why: string) {
88 blocked += 1
89 $.ui.toast(`🧱 script-gate: ${why}`)
90 $.ui.status(`🧱 سكربتات محجوبة: ${blocked}`)
91 return {
92 deny:
93 `script-gate: blocked — ${why}. ` +
94 'Running code straight from the internet is not allowed here. ' +
95 'Download it to a file instead, show the user the full URL and the script contents, ' +
96 'and run it only after the user explicitly approves.',
97 }
98}
99
100export const register: Register = on => {
101 on('tool.call', { tool: 'Bash' }, ($, e, next) => {
102 const why = verdict(e.command)
103 return why === undefined ? next(e) : deny($, why)
104 })
105}
106