""" 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)" == "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" &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:]))