lockdown.py
Rama main · réplica de lectura del repositorio original.
"""
Company-wide lockdown: the policy files IT pushes to every developer machine (MDM, Intune, Jamf, Ansible...)
so each coding agent runs Squidbrake's hook and a developer can't switch it off.
squidbrake lockdown --url https://gateway.yourcompany.com --out squidbrake-lockdown
writes one folder per agent, each in that vendor's own managed-settings format, plus a README saying where each
file goes on macOS, Linux and Windows. One policy (rules.yaml on the gateway) then decides for every agent:
the hooks only ask the gateway. What each vendor file adds on top:
claude-code managed-settings.json: Squidbrake's hooks, only managed hooks run, no --dangerously-skip-permissions,
and a short native deny list as a backstop
codex requirements.toml: Squidbrake's managed hook, only managed hooks run, hooks forced on, no
danger-full-access sandbox (no --yolo)
gemini-cli settings.json (system): Squidbrake's BeforeTool hook, YOLO mode off; policies/squidbrake.toml: the
same backstop as an admin-tier policy
cursor hooks.json (enterprise): Squidbrake's hooks with failClosed
Every machine also needs Squidbrake installed (`pipx install squidbrake`) and its own agent key in the
GATEWAY_API_KEY environment variable (made in the dashboard's Team tab with "Works for" set to that person).
Keys are never written into these files: one file goes to every machine, and a key in it would be everyone's.
Formats follow each vendor's managed-settings documentation as of October 2026; check `squidbrake lockdown`'s README
against your agent versions before a fleet-wide rollout.
"""
from __future__ import annotations
import json
from pathlib import Path
AGENTS = ("claude-code", "codex", "gemini-cli", "cursor")
# Catastrophic commands blocked by the agent itself as well, so they stay blocked even if the hook process
# can't start (Claude Code and Codex let a call through when a command hook crashes or times out).
# Kept short and exact on purpose: in Claude Code's rules `*` is a wildcard and Gemini's commandPrefix matches
# prefixes, so `rm -rf /*` or a prefix `rm -rf /` there would also block `rm -rf /tmp/build`.
# Everything else is judged by the gateway, with your rules and history.
CLAUDE_DENY = ["rm -rf /", "rm -rf ~", "rm -rf ~/", "rm -rf $HOME", "sudo rm -rf /"]
GEMINI_DENY_REGEX = [r"^\s*(sudo\s+)?rm\s+-(rf|fr)\s+(/|/\*|~|~/|~/\*|\$HOME)\s*$",
r"^\s*(sudo\s+)?mkfs(\.\w+)?\s",
r"^\s*(sudo\s+)?dd\s.*\bof=/dev/(sd|nvme|disk|hd)"]
WHERE = {
"claude-code": {"macOS": "/Library/Application Support/ClaudeCode/managed-settings.json",
"Linux / WSL": "/etc/claude-code/managed-settings.json",
"Windows": r"C:\Program Files\ClaudeCode\managed-settings.json",
"MDM instead": "macOS: plist domain com.anthropic.claudecode; Windows: REG_SZ value 'Settings' under "
r"HKLM\SOFTWARE\Policies\ClaudeCode (the JSON as its value)"},
"codex": {"macOS / Linux": "/etc/codex/requirements.toml",
"Windows": r"%ProgramData%\OpenAI\Codex\requirements.toml",
"also": "create the hooks folder it names (/etc/squidbrake/codex-hooks, or "
r"C:\ProgramData\squidbrake\codex-hooks); Codex checks that it exists"},
"gemini-cli": {"macOS": "/Library/Application Support/GeminiCli/settings.json and .../GeminiCli/policies/squidbrake.toml",
"Linux": "/etc/gemini-cli/settings.json and /etc/gemini-cli/policies/squidbrake.toml",
"Windows": r"C:\ProgramData\gemini-cli\settings.json and C:\ProgramData\gemini-cli\policies\squidbrake.toml",
"also": "the policies folder must be owned by root (Windows: not writable by standard users)"},
"cursor": {"macOS": "/Library/Application Support/Cursor/hooks.json", "Linux / WSL": "/etc/cursor/hooks.json",
"Windows": r"C:\ProgramData\Cursor\hooks.json"},
}
def _hook_args(url: str, *extra: str) -> list[str]:
return [*extra, "--url", url]
def _cmd(url: str, *extra: str) -> str:
return " ".join(["squidbrake", *_hook_args(url, *extra)])
def claude_code(url: str) -> dict:
hook = {"type": "command", "command": "squidbrake", "args": _hook_args(url, "hook", "--source", "claude-code")}
return {
"hooks": {
"PreToolUse": [{"matcher": "*", "hooks": [{**hook, "timeout": 600}]}],
"PostToolUse": [{"matcher": "*", "hooks": [{**hook, "timeout": 30}]}],
"PostToolUseFailure": [{"matcher": "*", "hooks": [{**hook, "timeout": 30}]}],
"UserPromptSubmit": [{"hooks": [{**hook, "timeout": 15}]}],
},
"allowManagedHooksOnly": True,
"permissions": {
"disableBypassPermissionsMode": "disable",
"deny": [f"Bash({c})" for c in CLAUDE_DENY],
},
}
def codex(url: str) -> str:
cmd = _cmd(url, "agent-hook", "codex")
return f'''# Squidbrake lockdown for Codex: install as requirements.toml (see README.md)
allowed_sandbox_modes = ["read-only", "workspace-write"] # no danger-full-access, so no --yolo
allow_managed_hooks_only = true
[features]
hooks = true # managed hooks run even if a user turns hooks off
[hooks]
managed_dir = "/etc/squidbrake/codex-hooks"
windows_managed_dir = 'C:\\ProgramData\\squidbrake\\codex-hooks'
[[hooks.PreToolUse]]
matcher = "Bash|shell|apply_patch|Edit|Write"
[[hooks.PreToolUse.hooks]]
type = "command"
command = "{cmd}"
command_windows = "{cmd}"
timeout = 600
statusMessage = "Squidbrake: checking"
'''
def gemini_settings(url: str) -> dict:
return {
"hooks": {"BeforeTool": [{"matcher": "run_shell_command|write_file|replace|read_file|read_many_files",
"hooks": [{"name": "squidbrake", "type": "command",
"command": _cmd(url, "agent-hook", "gemini-cli"), "timeout": 600000}]}]},
"security": {"disableYoloMode": True},
}
def gemini_policy() -> str:
rules = "\n".join(f'''[[rule]]
toolName = "run_shell_command"
commandRegex = {json.dumps(c)}
decision = "deny"
priority = 900
denyMessage = "Blocked by company policy (Squidbrake backstop)"
''' for c in GEMINI_DENY_REGEX)
return "# Squidbrake backstop, admin tier: these stay blocked even if the hook can't run\n\n" + rules
def cursor(url: str) -> dict:
cmd = _cmd(url, "agent-hook", "cursor")
entry = {"command": cmd, "type": "command", "timeout": 600, "matcher": "", "failClosed": True}
return {"version": 1, "hooks": {"beforeShellExecution": [entry], "beforeReadFile": [entry]}}
def readme(url: str, agents: list[str]) -> str:
lines = [
"# Squidbrake lockdown", "",
f"These files make every developer machine send its coding agents' actions to {url} and stop a",
"developer from switching that off. Push them with your device management (Jamf, Intune, Kandji, Ansible...).", "",
"On every machine:", "",
"1. Install Squidbrake: `pipx install squidbrake` (the `squidbrake` command must be on PATH for the agent).",
"2. Set `GATEWAY_API_KEY` to that person's own agent key (dashboard -> Team -> Add -> AI agent, \"Works for\" = them).",
" One key per person means the dashboard shows whose agent did what, and second-person approval works.",
"3. Copy each file below to its place. Restart the agents.", "",
"Check a machine with `squidbrake connect status`.", "",
]
for a in agents:
lines += [f"## {a}", ""] + [f"- **{k}:** `{v}`" if k not in ("also", "MDM instead") else f"- {k}: {v}"
for k, v in WHERE[a].items()] + [""]
lines += [
"## What to know", "",
"- The hook asks the gateway; if the gateway can't be reached, the hook blocks the action (fail closed).",
"- If the hook itself can't start (Squidbrake not installed, PATH wrong), Claude Code and Codex let the call",
" through. The short native deny list in these files still blocks the catastrophic commands in that case.",
" Cursor's hooks use failClosed, so they block.",
"- Codex runs a new hook only after it's trusted; managed hooks from requirements.toml are trusted by policy.",
"- Vendors change these formats. Test on one machine per agent before a fleet-wide rollout.", "",
]
return "\n".join(lines)
def write(url: str, out: Path, agents: list[str] | None = None) -> list[Path]:
"""Write the lockdown files under out/. Returns the files written."""
url = url.rstrip("/")
if not url.startswith(("http://", "https://")):
raise ValueError("the gateway URL must start with http:// or https://")
agents = [a for a in (agents or AGENTS)]
unknown = [a for a in agents if a not in AGENTS]
if unknown:
raise ValueError(f"no lockdown for {', '.join(unknown)}; supported: {', '.join(AGENTS)}")
files: dict[str, str] = {}
if "claude-code" in agents:
files["claude-code/managed-settings.json"] = json.dumps(claude_code(url), indent=2) + "\n"
if "codex" in agents:
files["codex/requirements.toml"] = codex(url)
if "gemini-cli" in agents:
files["gemini-cli/settings.json"] = json.dumps(gemini_settings(url), indent=2) + "\n"
files["gemini-cli/policies/squidbrake.toml"] = gemini_policy()
if "cursor" in agents:
files["cursor/hooks.json"] = json.dumps(cursor(url), indent=2) + "\n"
files["README.md"] = readme(url, agents)
written = []
for rel, body in files.items():
p = out / rel
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(body, encoding="utf-8")
written.append(p)
return written