Block remote script piped to shell
No "curl | sh" runs unaudited code on your machine
Blocks the supply-chain classic — curl/wget piped straight into sh/bash, including PowerShell iwr|iex and shell substitutions like sh -c "$(curl …)". The agent must download, inspect, then run. Quoted mentions are ignored to avoid false positives.
What does the Block remote script piped to shell hook do?
Block remote script piped to shell is a Claude Code PreToolUse hook matching Bash. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. No "curl | sh" runs unaudited code on your machine.
As a PreToolUse hook it runs before the action completes, so it can block or adjust what Claude is about to do. Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.
Use cases
- Supply-chain safety
- Untrusted install scripts
- Hardened CI agents
Tags
settings.json fragment
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/block-curl-pipe-sh.mjs",
"type": "command"
}
],
"matcher": "Bash"
}
]
}
}Script · .claude/hooks/block-curl-pipe-sh.mjs
#!/usr/bin/env node
// Bloque l'exécution de scripts distants non audités : curl|wget … | sh (PreToolUse Bash)
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
// Tuyau d'un téléchargeur vers un shell — le vecteur supply-chain n°1.
// Testés sur la commande NETTOYÉE (le contenu entre guillemets est neutralisé)
// pour éviter les faux positifs (ex. git commit -m "how to curl | sh").
const PIPED = [
// curl/wget/fetch … | (sudo) sh|bash|zsh|dash|fish
/\b(?:curl|wget|fetch)\b[^|]*\|\s*(?:sudo\s+)?(?:ba|z|da|fi)?sh\b/i,
// PowerShell : iwr|curl|Invoke-WebRequest … | iex|Invoke-Expression
/\b(?:iwr|curl|invoke-webrequest)\b[^|]*\|\s*(?:iex|invoke-expression)\b/i,
];
// Substitutions exécutées par le shell même à l'intérieur de guillemets doubles
// (sh -c "$(curl …)") : testées sur la commande BRUTE.
const SUBSTITUTION = [
/\b(?:ba|z|da|fi)?sh\b[^\n]*<\(\s*(?:curl|wget|fetch)\b/i, // bash <(curl …)
/\b(?:ba|z|da|fi)?sh\b[^\n]*\$\(\s*(?:curl|wget|fetch)\b/i, // sh -c "$(curl …)"
];
// Retire les chaînes entre guillemets pour éviter les faux positifs.
function stripQuotedArgs(cmd) {
return cmd.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
}
export function run(input) {
if (input.tool_name && input.tool_name !== 'Bash') return null;
const command = input.tool_input?.command ?? '';
const stripped = stripQuotedArgs(command);
const piped = PIPED.some((p) => p.test(stripped));
const substituted = SUBSTITUTION.some((p) => p.test(command));
if (!piped && !substituted) return null;
return {
decision: 'block',
reason:
"Exécution d'un script distant via pipe bloquée (curl|wget … | sh). " +
'Téléchargez le script dans un fichier, inspectez-le, puis lancez-le : ' +
'curl -fsSL <url> -o /tmp/install.sh && less /tmp/install.sh && sh /tmp/install.sh',
};
}
/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const input = JSON.parse(readFileSync(0, 'utf8'));
const result = run(input);
if (result) process.stdout.write(JSON.stringify(result));
}
Learn more
Related hooks
- Secret detection before Bash executionCatch a leaked API key before it ever runs
- Destructive command blockingStops a disk-wiping shell command before it runs
- Sensitive file write protectionYour .env and keys stay untouched by the agent
- Lock file write protectionLock files stay intact - package manager only