{"slug": "shellfirm-a-seatbelt-for-ai-agents-running-shell-commands", "title": "Shellfirm – a seatbelt for AI agents running shell commands", "summary": "Shellfirm, an open-source tool from developer Elad Kaplan, intercepts dangerous shell commands before AI agents or humans execute them, shipping with 496 built-in checks across 23 groups enabled by default. The tool installs shell hooks for 8 shells — Bash, Zsh, Fish, Elvish and Xonsh tested end to end, Nushell tested with a limitation, and PowerShell and Oils shipped but not yet tested — and escalates challenges based on context such as SSH sessions, root access, protected git branches and production Kubernetes clusters. Shellfirm also logs every intercepted command and decision as JSON lines with secrets redacted, and enforces team policies through a committed .shellfirm.yaml or an org-wide baseline at /etc/shellfirm/policy.yaml that personal settings cannot weaken.", "body_md": "**Think before you execute.**\n\nHumans make mistakes. AI agents make them faster. shellfirm intercepts dangerous shell commands before the damage is done.\n\n```\n% git push origin main --force\n============ RISKY COMMAND DETECTED ============\nCommand: git push origin main --force\nCheck: git:force_push\nSeverity: HIGH\nBlast radius: [PROJECT] — Force-pushes to origin/main\nContext: branch=main, target=main\nDescription: This force-pushes and can overwrite remote history.\nAlternative: git push --force-with-lease\n(Checks that your local ref is up-to-date before force pushing, preventing accidental overwrites of others' work.)\nEscalated to Yes (from Math): protected branch `main`\n\n[HIGH] Type `yes` to continue (Esc or Ctrl-C to cancel)\n```\n\nThis is real output, captured in zsh with default settings on the protected\n`main` branch. (In bash the banner starts after a blank line. Off a protected branch the challenge is not escalated, and the banner\nalso tells you how to stop being asked: `shellfirm config ignore <id>`; that\nhint is left out when the context raised the challenge.) The\nexact output depends on the command, your settings and where you run it. The\nchallenge is a small math problem, pressing Enter, or typing `yes`: the higher\nthe severity and the riskier the context (protected branch, SSH, root,\nproduction cluster), the stricter the challenge. Commands on a deny list are\nblocked outright. Prefer no math? See [Accessibility](#accessibility).\n\n- **496 built-in checks across 23 groups** (filesystem, git, Kubernetes, Terraform, Docker, AWS, GCP, Azure, Heroku, Fly.io, Vercel, Netlify, GitHub, npm, databases, Redis, MongoDB, network, remote-script execution such as`curl … | bash` (`shell` ), credential exfiltration (`exfil` ), …), all on by default. List them with`shellfirm check --list --all` ; the generated[check catalogue](https://github.com/kaplanelad/shellfirm/blob/main/docs/checks/README.md) has the exact, current count.\n- **Shell hooks for 8 shells** — Bash, Zsh, Fish, Elvish and Xonsh are tested end to end; Nushell is tested with a limitation (see[Shell support](#shell-support) ); the PowerShell and Oils hooks are shipped but**not yet tested** end to end.`shellfirm status` shows which shells have the hook installed.\n- **Context-aware escalation** — harder challenges when connected via SSH, running as root, on protected git branches, or in production Kubernetes clusters\n- **Safer alternatives** — many checks suggest a safer command (e.g.`--force-with-lease` )\n- **Severity levels** with configurable thresholds (`Critical` ,`High` ,`Medium` ,`Low` ,`Info` )\n- **Team and org policies** — commit a`.shellfirm.yaml` with rules that everyone in the repo gets, or install an org-wide baseline at`/etc/shellfirm/policy.yaml` . Policies are additive and enforced: personal settings cannot weaken them. See[docs/team-policies.md](https://github.com/kaplanelad/shellfirm/blob/main/docs/team-policies.md) .\n- **Audit trail** — every intercepted command and decision is logged as JSON lines (user, host, repo, branch, outcome, and which policy denied it), with secrets redacted and bounded rotation, plus filters and a table view:`shellfirm audit show --since 7d --outcome denied` . See[docs/audit.md](https://github.com/kaplanelad/shellfirm/blob/main/docs/audit.md) .\n- **Blast radius** — shows what a command would affect (files, branches, resources), within one total time budget per command (`blast_radius_budget_ms` , default 400 ms, at most 5000;`shellfirm config impact-budget <ms>` ) so the prompt never waits long; a count that runs out of time says`more than 120,000 files (stopped counting)` , a step it could not start says`skipped, no time left` . When it takes longer than 200 ms a transient`shellfirm: checking impact… (Ctrl-C cancels)` line shows; Ctrl-C there cancels the command and records it in the audit log\n- **Uncommitted work counts** — a recursive delete of a folder that holds uncommitted or untracked files (git cannot bring them back) is High, so a relaxed`min_severity` still asks\n- **Feedback after you confirm** — a passed challenge prints`✓ approved — running: <command>` (dim, on the terminal only), so a slow command such as`helm uninstall` does not look hung. Turn it off with`shellfirm config approved-notice off`\n- **MCP server and agent hooks** — use shellfirm as a guardrail for Claude Code, Cursor and other agents\n- **LLM analysis (experimental)** — configured with`shellfirm config llm` ; used only to review AI-agent commands when`agent.llm_review` is enabled\n\nOne command sets up both automatic safety (hooks) and on-demand analysis (MCP):\n\n```\nshellfirm connect claude-code\n```\n\nThis adds:\n\n- **Hooks** — every Bash command is checked before execution; risky commands are blocked\n- **MCP** — Claude can call shellfirm tools to explain risks and suggest alternatives\n\nSafe commands get no answer from the hook, so Claude Code's own permission\nrules decide whether it asks you (the shellfirm 0.3.10 hook approved them\noutright). After upgrading from 0.3.10, re-run `shellfirm connect claude-code`; see [Upgrading from 0.3.10](https://github.com/kaplanelad/shellfirm/blob/main/CHANGELOG.md#upgrading-from-0310).\n\n| Tool | Description | \n|---|---|\n| `check_command` | Check if a command is risky — returns severity, matched rules, and alternatives | \n| `suggest_alternative` | Get safer replacement commands | \n| `explain_risk` | Detailed explanation of why a command is dangerous | \n| `get_policy` | Read the active shellfirm configuration | \n\nUpgrading from 0.3.10? Upgrading the package only replaces the binary. Read [Upgrading from 0.3.10](https://github.com/kaplanelad/shellfirm/blob/main/CHANGELOG.md#upgrading-from-0310): restart your shells, re-run `shellfirm init <shell>` for elvish, xonsh and nushell and `shellfirm connect <agent>` for each agent, fix CI scripts for the new exit codes, and enable the new default groups (`shellfirm status` shows the command).\n\n```\nnpm install -g @shellfirm/cli\nbrew tap kaplanelad/tap && brew install shellfirm\ncargo install shellfirm\n```\n\nFor prebuilt binaries through Cargo's binstall workflow:\n\n```\ncargo binstall shellfirm\n```\n\nOr download the binary from the [releases page](https://github.com/kaplanelad/shellfirm/releases).\n\n**1. Install the shell hook:**\n\n```\nshellfirm init            # installs the hook for your current shell, then offers the others [y/N]\nshellfirm init zsh        # or only for one shell\nshellfirm init --all      # every supported shell\nshellfirm init --dry-run  # preview what would change\nshellfirm init --yes      # no questions (scripts, provisioning); sets the recommended Balanced level\n```\n\n`shellfirm init` asks you to confirm, then asks two questions: the challenge\ntype (Math, Enter or Yes) and the protection level.\n\n**2. Restart your shell** (`exec zsh`, `exec bash`, … or open a new terminal).\n\n**3. Check it:**\n\n```\nshellfirm status        # shows hooks, settings, policy and thresholds\nshellfirm doctor        # checks that a new shell really routes commands through shellfirm\ngit reset --hard        # should trigger shellfirm\n```\n\n| Shell | Status | \n|---|---|\n| Bash, Zsh, Fish, Elvish, Xonsh | Tested: risky commands are challenged before they run. Elvish `fn` functions are not expanded before the check, and xonsh Python code that starts programs from computed values is not checked. | \n| Nushell | Tested, with a limitation: Nushell cannot cancel a command line from a hook, so **Nushell built-in commands (for example `rm`, `mv`) still run** even after you cancel the challenge. External programs are covered. Treat Nushell protection as a warning, not a guard. | \n| PowerShell, Oils ( `osh` ) | Hooks are shipped but not yet tested end to end. Please report what works. | \n\nThe shell hooks check what you type at the interactive prompt. AI agents\nshould use the agent hooks (`shellfirm connect`), not the shell hook.\n\n**What the hook sees.** The hooks expand what they can before checking:\naliases (including chains such as `del` → `rmrf` → `rm -rf`), history\nreferences (`!!`, `!$`), and the body of a small shell function you call\n(`nuke() { rm -rf \"$@\"; }; nuke build`), so the banner's `Command:` line\nshows what will really run. In bash, zsh and fish every function a line\ncalls is followed, however deep, with the call's arguments (also after\n`shift`), within a budget of 64 functions / 64 KB (16 KB for one body);\npast it the line gets a Medium `shell:function_depth_limit` challenge\ninstead of being skipped. A function over 16 KB that was loaded from a file\n(nvm, sdkman, conda) is treated like a script you run: its body is not read,\nso `nvm use 18` is not prompted, and neither is a call whose danger is only in\nthe body (`bigclean ~` with `rm -rf \"$1\"` inside); a command it is asked to run\n(`nvm exec 18 rm -rf ~/data`) is still checked. One typed or pasted into the\nsession gets the Medium challenge. Nushell follows aliases and `def` s, and\nxonsh alias chains up to 10;\nelvish `fn` functions are not expanded. Limits: the contents of scripts you\nrun (`./cleanup.sh`) are not inspected, and commands built at run time\n(`$CMD`, `eval \"$(…)\"` output) can only be checked as far as they are\nvisible on the line. shellfirm is a guard against mistakes, not a sandbox.\n\n**bash `fc`.** bash turns the `fc` builtin off and routes `command fc`\nthrough `command_not_found_handle` to the checked `fc`, so it runs in a\nsubshell; `builtin fc` works only on a line typed at the prompt.\n\n**After a blocked or cancelled line.** In bash, zsh and fish, `$?`/`$status`\nafter a line shellfirm blocked is still the previous command's status: the\nline never ran, and a key binding cannot set it. nushell sets\n`$env.LAST_EXIT_CODE = 1`. The line is kept in the editor instead (Ctrl-U\nclears it). Scripts and CI should use\n`shellfirm check --exit-code` (exit 10 / 11) or `shellfirm pre-command`\n(exit 65 / 77), not `$?` of an interactive prompt.\n\n```\nshellfirm check -c 'git reset --hard'           # matched check ids, severity, alternatives\nshellfirm check -c 'git reset --hard' --format json\n```\n\nThe text output ends with what your shell would do (`Shell (you, in a terminal): would challenge (Math)`) and what an AI-agent hook would answer\n(`AI agent (hooks, MCP): deny`), so a command you can run after a math problem\nis not mistaken for one that is blocked for you. In CI, `--strict` also exits\n78 when your settings, custom checks or a policy have warnings.\n\nshellfirm has no \"allow once\" switch: to run a flagged command, solve the challenge. To stop being asked about a check permanently, ignore its id:\n\n```\nshellfirm config ignore git:reset            # never prompt for this check\nshellfirm config ignore --remove git:reset   # undo\nshellfirm config deny git:force_push         # always block this check\nshellfirm config severity High               # only prompt for High and Critical\n```\n\nUnknown ids are rejected (with suggestions), so a typo cannot leave you\nsilently unprotected. Checks required by a project's `.shellfirm.yaml` cannot\nbe ignored or disabled for that project (`shellfirm policy show` lists them).\n\n```\nshellfirm config          # interactive editor (TUI)\nshellfirm config show     # current settings\nshellfirm config edit     # open settings.yaml in $EDITOR (invalid edits are rejected)\nshellfirm config reset    # back up the current file, then start over\n```\n\nSettings live in `~/.config/shellfirm/settings.yaml` (on macOS:\n`~/Library/Application Support/shellfirm/settings.yaml`). An invalid settings\nfile is reported with its line number and is never overwritten; the shell hook\nkeeps working with safe defaults for the invalid values. See\n[docs/config.md](https://github.com/kaplanelad/shellfirm/blob/main/docs/config.md).\n\n`--log <level>` (or `SHELLFIRM_LOG=debug`) prints diagnostics to stderr.\n`RUST_LOG` is not used.\n\n**No math challenges.** If mental arithmetic is hard for you (dyscalculia, a\nscreen reader, fatigue), use these three settings. Medium (and, if you\nprompt for them, Low and Info) commands then need only Enter, and everything\nthat would have asked for math asks you to type `yes` instead, including when\nSSH, root or a protected branch raises the risk:\n\n```\nshellfirm config challenge Enter\nshellfirm config escalation severity --critical Yes --high Yes --medium Enter --low Enter --info Enter\nshellfirm config context escalation --elevated Yes --critical Yes\n```\n\nCheck the result with `shellfirm pre-command -t -c 'git push -f origin main'`\n(it prints the effective challenge). A project's `.shellfirm.yaml` or a\nper-group or per-check override can still ask for `Math`. `shellfirm policy show`\nand `shellfirm config show` list them.\n\n**Plain output.** The challenge is linear text with one labelled fact per line,\nand severity is written as a word. `NO_COLOR=1` (any non-empty value; an\nempty `NO_COLOR=` keeps colour), `CLICOLOR=0` and a monochrome terminal\n(`TERM=dumb`, `vt100`, `xterm-mono`, …) turn colour off everywhere: the\nchallenge, the CLI and the settings editor. `CLICOLOR_FORCE=1` forces it on.\nWith a non-UTF-8 locale (`LC_ALL=C`), `status`, `config show`, `policy show`\nand `audit show` print ASCII instead of `—`, `→` and `✓`. There are no\ntimeouts, and retries are unlimited.\n\n**Settings editor (`shellfirm config`).** Everything is reachable by keyboard\n(`?` shows the keys). The active tab is shown as `[Name]` and the selected row\nis marked with `>`, so focus is visible without colour. With `NO_COLOR`,\nhighlights use reverse video. With a non-UTF-8 locale (for example\n`LC_ALL=C`), symbols fall back to ASCII. Text fields place the terminal cursor\nat the insertion point. In windows smaller than 80x24, tabs scroll to the\nselected row (the minimum is 60x16). Under `TERM=dumb` the editor does not\nstart; use `shellfirm config show` and the `shellfirm config <subcommand>`\ncommands instead.\n\nshellfirm is a safety net for mistakes, not a sandbox: it reads the command line before it runs and cannot see what a program does once started. Some risky commands have no rule yet:\n\n- Reverse shells: only the common spellings are recognised; other spellings are not.\n- Encoded or downloaded text that is decoded and piped or passed straight into a shell or interpreter is not always followed.\n- Sending secrets (keys, tokens, credential files) to another host with\n`curl` ,`aws` or`gh` is not detected in general.\n- Reading a private key or credentials file is flagged (Medium), also with\nClaude Code's `Read` tool, but reading a`.env` file and listing`~/.ssh` are not. Add a[custom check](https://github.com/kaplanelad/shellfirm/blob/main/docs/config.md#custom-checks) if you want to\nbe asked before an agent reads them, for example`test: '^(?:cat|less|head|tail|bat)\\s+(?:\\S+\\s+)*?\\S*\\.env(?:\\.(?:local|development|production|test))?(?:\\s|$)'` .\n- A command that uses a token without printing it (`GH_TOKEN=$(gh auth token) gh api …` ,`gh auth token | docker login --password-stdin` ) is still asked\nlike printing one; approve it. Checks cannot yet be limited to AI agents:`config ignore` turns a check off for the shell and the agents together.\n- A destructive call made through an API with `curl` (a GraphQL mutation that\ndeletes a volume, a`DELETE` request to a cloud API) is not recognised.\n- Lock-out commands beyond locking the root account (locking or removing other users, firewall rules that drop the SSH port) are not checked.\n- `git -c` with a program value (`core.pager` ,`core.editor` ) is Medium, and`-c diff.external=` /`-c credential.helper=` are not matched; neither are`git config include.path …` and`git config init.templateDir …` , which make\nlater git commands run another file's settings or hooks.\n- Commands fed on standard input to a remote or root shell (`ssh host <<EOF … EOF` ,`echo 'rm -rf /srv' | sudo bash` ,`ssh host < script.sh` ) are not read\nwhen they come from a file or another program.\n- False positive: hardening a firewall the right way (`ufw allow OpenSSH && ufw default deny incoming` ) is still treated as a possible lock-out (High; an\nagent is denied). Run the two commands one at a time, or approve it.\n\nNushell built-in commands are not stopped (see [Shell support](#shell-support)).\nReport a missing case as an issue; the [check catalogue](https://github.com/kaplanelad/shellfirm/blob/main/docs/checks/README.md)\nlists what is covered.\n\n```\nshellfirm init --uninstall                     # remove the hooks from your shell rc files\nshellfirm connect claude-code --uninstall      # if you connected an AI tool\nshellfirm status                               # confirm: no shell shows \"installed\"\n```\n\n`status` reads your rc files, so it shows \"not installed\" right away; a shell\nthat already loaded the hook keeps it until you restart that shell.\n\nIf you added the hook by hand (`eval \"$(shellfirm init bash)\"` and similar),\ndelete that line from your rc file. Restart open shells: a running shell keeps\nthe hook until it exits.\n\nThen remove the binary the way you installed it (`npm uninstall -g @shellfirm/cli`,\n`brew uninstall shellfirm`, or `cargo uninstall shellfirm`) and, if you no\nlonger need them, delete your settings, custom checks and audit log:\n`rm -rf ~/.config/shellfirm` (macOS: `~/Library/Application Support/shellfirm`).\n\n- [Configuration](https://github.com/kaplanelad/shellfirm/blob/main/docs/config.md) — settings file, CLI, custom checks\n- [Team policies](https://github.com/kaplanelad/shellfirm/blob/main/docs/team-policies.md) —`.shellfirm.yaml` project-level rules\n- [Audit trail](https://github.com/kaplanelad/shellfirm/blob/main/docs/audit.md) — record format, filters, rotation\n- [Check catalogue](https://github.com/kaplanelad/shellfirm/blob/main/docs/checks/README.md) — every built-in check, by group\n- [Adding built-in patterns](https://github.com/kaplanelad/shellfirm/blob/main/docs/add-new-patterns.md) — contributing new checks\n\nMore guides are on **[shellfirm.vercel.app](https://shellfirm.vercel.app)**.\n\nContributions are welcome. [CONTRIBUTING.md](https://github.com/kaplanelad/shellfirm/blob/main/CONTRIBUTING.md) covers the build,\nthe tests and the checklist for a pull request, and\n[docs/add-new-patterns.md](https://github.com/kaplanelad/shellfirm/blob/main/docs/add-new-patterns.md) walks through adding a\ncheck. Found a bypass? Please report it privately; see [SECURITY.md](https://github.com/kaplanelad/shellfirm/blob/main/SECURITY.md).", "url": "https://wpnews.pro/news/shellfirm-a-seatbelt-for-ai-agents-running-shell-commands", "canonical_source": "https://github.com/kaplanelad/shellfirm", "published_at": "2026-10-29 07:13:30+00:00", "updated_at": "2026-10-29 07:50:11.463408+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools"], "entities": ["Shellfirm", "Elad Kaplan", "GitHub", "Kubernetes", "Terraform", "Docker", "PowerShell", "Nushell"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/shellfirm-a-seatbelt-for-ai-agents-running-shell-commands", "markdown": "https://wpnews.pro/news/shellfirm-a-seatbelt-for-ai-agents-running-shell-commands.md", "text": "https://wpnews.pro/news/shellfirm-a-seatbelt-for-ai-agents-running-shell-commands.txt", "jsonld": "https://wpnews.pro/news/shellfirm-a-seatbelt-for-ai-agents-running-shell-commands.jsonld"}}