HookStack
Back to catalogue
SecurityPreToolUse· BashPreToolUseBefore tool execution · can block⚡ blocking

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

#security#bash#supply-chain#curl#remote-code-execution#guardrail

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