SLOPSHOPPER

script-gate

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

newguardtoaststatus
v0.2.0MITupdated 2026-10-06ABDUAZIZX/script-gate
A shopper browsing a rack in a slop shop
README

script-gate 🧱

A Claude Code mod (v2.1.287+) that stops Claude from running scripts straight from the internet.

Mod لـ Claude Code يمنع Claude من تشغيل أي سكربت يُنزَّل من الإنترنت ويُنفَّذ مباشرة — أسلوب شائع في نشر البرمجيات الخبيثة. يوقف الأمر قبل التنفيذ، ويوجّه Claude إلى الطريقة الآمنة: نزّل الملف، اعرضه على المستخدم، ولا تشغّله إلا بموافقته.

Why a mod and not just instructions?

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.

What it blocks (Bash tool)

PatternExample
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 commandcurl -o i.sh … && bash i.sh, …; chmod +x i.sh; ./i.sh
Download inside command/process substitutionbash -c "$(curl …)", bash <(curl …), `curl … \tee >(bash)`
PowerShell iex on remote contentiex (New-Object Net.WebClient).DownloadString(…)
Encoded PowerShellpowershell -EncodedCommand …
Windows LOLBins used as downloaderscertutil -urlcache, bitsadmin /transfer, mshta http…, regsvr32 /i:http…
Python executing downloaded codeexec(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.

Install

/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

Test

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).

Limits

  • Pattern-based: a determined attacker can obfuscate further (variables, aliases, a download in one call and the run in a later one). This is a safety net, not a sandbox.
  • The data-consumer exemption is a deny-list of code-execution words; it errs towards blocking.
  • It guards the model's Bash calls, not commands you type yourself with !.
  • Mods are an early-access API and may change between releases. Read any mod's code before installing it.

Also see env-guard — blocks Claude from reading .env secrets.

Changelog

  • 0.2.0 — catches interpreters behind intermediate pipes (| 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).
  • 0.1.0 — first release.

MIT License

Source 1 files
hooks/register.ts 106 lines
1import 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