shell_guard.py
Rama main · réplica de lectura del repositorio original.
"""
A guard for the lines people type or paste into their own terminal (no agent involved, so no agent hook sees them).
squidbrake shell-guard check "rm -rf ~/" exit 0 run, 1 block, 2 ask (the reason goes to stderr)
squidbrake shell-guard install --shell bash prints the snippet for ~/.bashrc (also: zsh, powershell)
`check` is a thin layer over commands.read(), so the terminal and the agent hooks always agree:
catastrophic -> block, irreversible or hidden -> ask, anything else -> run (silently).
`install` only prints, unless you add --write (or say yes when `squidbrake setup` asks): then it adds the snippet to
your shell's startup file between two marker lines, after a backup, and `uninstall` takes exactly that out again.
It is a helper for people, not a gate: if the check itself fails (an exception, `squidbrake` missing), the line
runs. Local only; nothing is sent anywhere.
"""
from __future__ import annotations
import sys
RUN, BLOCK, ASK = "run", "block", "ask"
EXIT_CODES = {RUN: 0, BLOCK: 1, ASK: 2}
SHELLS = ("bash", "zsh", "powershell")
def check(line: str) -> tuple[str, str]:
"""(action, message) for one command line. The message is empty when the action is run."""
import os
import commands
import runs
reading = commands.read(line)
kind = reading.kind
why = reading.summary() or line.strip()[:80]
try:
underneath = runs.expand(line, os.getcwd())
except Exception:
underneath = [] # fail open: decide on the line alone
for item in underneath: # what the line runs underneath, read from files on disk
via = item.get("via") or "what it runs"
for inner in item.get("lines") or []:
try:
inner_reading = commands.read(inner)
except Exception:
continue
if inner_reading.kind in ("catastrophic", "irreversible", "hidden") and \
commands.SEVERITY.index(inner_reading.kind) > commands.SEVERITY.index(kind):
kind = inner_reading.kind # it can only make the decision stricter, never looser
why = f"runs {via}, which {inner_reading.summary() or inner.strip()[:80]}"
if kind == "irreversible" and why == (reading.summary() or line.strip()[:80]) and reading.rebuildable:
return RUN, "" # your own rm -rf node_modules: the next build brings it back
if kind == "catastrophic":
return BLOCK, f"Squidbrake: blocked: {why}"
if kind == "irreversible":
return ASK, f"Squidbrake: this can't be undone: {why}"
if kind == "hidden":
return ASK, f"Squidbrake: {why}"
return RUN, ""
# --------------------------------------------------------------------------- snippets (printed, never installed)
# Each runs `squidbrake shell-guard check` before a line executes. Only exit codes 1 and 2 do anything, so a missing
# `squidbrake` (127) lets the line run, and main() turns any exception into 0. Interactive shells only.
BASH = r'''# Squidbrake terminal guard: checks each command line before it runs. Add to ~/.bashrc.
if [[ $- == *i* ]] && command -v squidbrake >/dev/null 2>&1; then
__sb_armed=0
__sb_arm() { __sb_armed=1; }
__sb_debug() {
# The DEBUG trap fires for every part of a pipeline; only the first one after the prompt checks the whole line.
[[ $__sb_armed == 1 ]] || return 0
__sb_armed=0
local line rc ans
line=$(HISTTIMEFORMAT= builtin history 1 | sed 's/^ *[0-9][0-9]*\*\{0,1\} *//')
[[ $line == *"$BASH_COMMAND"* ]] || line=$BASH_COMMAND # history skipped it (HISTCONTROL=ignorespace...)
squidbrake shell-guard check -- "$line" </dev/null
rc=$?
[[ $rc == 1 ]] && return 1
if [[ $rc == 2 ]]; then
read -r -p "Run it anyway? [y/N] " ans </dev/tty || return 1
[[ $ans == [yY]* ]] || return 1
fi
return 0
}
# Installed at the first prompt, when the rest of your rc files have loaded. trap -p can't see a DEBUG trap from inside
# a sourced file or a function, so this check is a plain PROMPT_COMMAND string. If one is already set (VS Code shell
# integration, bash-preexec, atuin...) it is left alone: replacing it would break that tool.
__sb_install='if [[ -z $__sb_done ]]; then __sb_done=1; if [[ -n $(trap -p DEBUG) ]]; then echo "Squidbrake: a DEBUG trap is already set (VS Code shell integration, bash-preexec...), so the terminal guard is off in this bash." >&2; else shopt -s extdebug; trap __sb_debug DEBUG; fi; fi'
if [[ "$(declare -p PROMPT_COMMAND 2>/dev/null)" == "declare -a"* ]]; then
PROMPT_COMMAND+=("$__sb_install" __sb_arm)
else
PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND;}$__sb_install;__sb_arm"
fi
fi
'''
ZSH = r'''# Squidbrake terminal guard: checks each command line before it runs. Add to ~/.zshrc.
if [[ -o interactive ]] && (( $+commands[squidbrake] )); then
(( $+widgets[__squidbrake_orig_accept_line] )) || zle -A accept-line __squidbrake_orig_accept_line
__squidbrake_accept_line() {
local rc=0
if [[ -n ${BUFFER//[[:space:]]/} ]]; then
zle -I
squidbrake shell-guard check -- "$BUFFER" </dev/null
rc=$?
if (( rc == 1 )); then
print # zle redraws the prompt one line up; without this it would overwrite the message
zle reset-prompt
return 0
elif (( rc == 2 )); then
local ans
read -q "ans?Run it anyway? [y/N] " </dev/tty || { print; zle reset-prompt; return 0; }
print
fi
fi
zle __squidbrake_orig_accept_line
}
zle -N accept-line __squidbrake_accept_line
fi
'''
POWERSHELL = r'''# Squidbrake terminal guard: checks each command line before it runs. Add to your profile ($PROFILE).
function global:__SquidbrakeAllow([string]$line) {
# True when the line may run. Any failure of the guard itself lets it run.
try {
if (-not $line -or -not $line.Trim()) { return $true }
$ErrorActionPreference = 'Continue'
$out = $line | squidbrake shell-guard check --stdin 2>&1
$rc = $LASTEXITCODE
if ($rc -ne 1 -and $rc -ne 2) { return $true }
Write-Host ''
Write-Host (($out | ForEach-Object { "$_" }) -join "`n")
if ($rc -eq 1) { return $false }
return ((Read-Host 'Run it anyway? [y/N]') -match '^[yY]')
} catch { return $true }
}
if ((Get-Command squidbrake -ErrorAction SilentlyContinue) -and (Get-Module PSReadLine)) {
Set-PSReadLineKeyHandler -Key Enter -ScriptBlock {
$line = $null; $cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
if (-not (__SquidbrakeAllow $line)) {
[Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($line) # up-arrow gets it back to edit
[Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
}
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
}
}
'''
SNIPPETS = {"bash": BASH, "zsh": ZSH, "powershell": POWERSHELL}
USAGE = """usage: squidbrake shell-guard check [--stdin] [--] LINE exit code 0 run, 1 block, 2 ask
squidbrake shell-guard install --shell bash|zsh|powershell [--write] print the snippet (--write: add it)
squidbrake shell-guard uninstall [--shell ...] take it out of your startup file again"""
BEGIN, END = "# >>> squidbrake terminal guard >>>", "# <<< squidbrake terminal guard <<<"
def this_shell() -> str:
import os
if os.name == "nt":
return "powershell"
name = os.path.basename(os.getenv("SHELL", ""))
return name if name in SHELLS else "bash"
def startup_file(shell: str):
"""The file that shell reads when a terminal opens."""
import os
import subprocess
from pathlib import Path
home = Path.home()
if shell == "zsh":
return home / ".zshrc"
if shell == "bash":
return home / (".bash_profile" if sys.platform == "darwin" else ".bashrc")
try: # PowerShell's own answer: Documents can be moved (OneDrive)
out = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", "$PROFILE"],
capture_output=True, text=True, timeout=20).stdout.strip()
if out:
return Path(out)
except (OSError, subprocess.SubprocessError):
pass
return Path(os.getenv("USERPROFILE", str(home))) / "Documents" / "WindowsPowerShell" / "Microsoft.PowerShell_profile.ps1"
def write(shell: str, path=None) -> str:
"""Add the snippet to the startup file (once), after a backup. -> what happened, in a sentence."""
import shutil
import time
path = path or startup_file(shell)
text = path.read_text(encoding="utf-8", errors="replace") if path.exists() else ""
if BEGIN in text:
return f"The terminal guard is already in {path}."
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists():
shutil.copy2(path, path.with_name(f"{path.name}.bak-{time.strftime('%Y%m%d-%H%M%S')}"))
block = f"{BEGIN}\n{SNIPPETS[shell].rstrip()}\n{END}\n"
path.write_text(text.rstrip("\n") + ("\n\n" if text.strip() else "") + block, encoding="utf-8")
return f"Added the terminal guard to {path}. New terminal windows warn before a command that can't be undone."
def remove(shell: str, path=None) -> str:
import re
path = path or startup_file(shell)
text = path.read_text(encoding="utf-8", errors="replace") if path.exists() else ""
if BEGIN not in text:
return f"The terminal guard isn't in {path}."
new = re.sub(re.escape(BEGIN) + r".*?" + re.escape(END) + r"\n?", "", text, flags=re.S)
path.write_text(new.rstrip("\n") + "\n" if new.strip() else "", encoding="utf-8")
return f"Took the terminal guard out of {path}."
def main(argv: list[str]) -> int:
if argv[:1] == ["check"]:
rest = argv[1:]
try:
if rest[:1] == ["--stdin"]:
line = sys.stdin.buffer.read().decode("utf-8-sig", "replace").strip() # PowerShell 5.1 prefixes a BOM
else:
line = " ".join(rest[1:] if rest[:1] == ["--"] else rest)
action, message = check(line)
except Exception:
return 0 # fail open: this is a helper for people, not a gate
if message:
print(message, file=sys.stderr)
return EXIT_CODES[action]
if argv[:1] in (["install"], ["uninstall"]):
shell = argv[argv.index("--shell") + 1] if "--shell" in argv[:-1] else (this_shell() if "--write" in argv
or argv[0] == "uninstall" else "")
if shell not in SHELLS:
print(USAGE, file=sys.stderr)
return 64
if argv[0] == "uninstall":
print(remove(shell))
elif "--write" in argv:
print(write(shell))
else:
sys.stdout.write(SNIPPETS[shell])
return 0
print(USAGE, file=sys.stderr)
return 64
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))