Think before you execute.
Humans make mistakes. AI agents make them faster. shellfirm intercepts dangerous shell commands before the damage is done.
% git push origin main --force
============ RISKY COMMAND DETECTED ============
Command: git push origin main --force
Check: git:force_push
Severity: HIGH
Blast radius: [PROJECT] — Force-pushes to origin/main
Context: branch=main, target=main
Description: This force-pushes and can overwrite remote history.
Alternative: git push --force-with-lease
(Checks that your local ref is up-to-date before force pushing, preventing accidental overwrites of others' work.)
Escalated to Yes (from Math): protected branch `main`
[HIGH] Type `yes` to continue (Esc or Ctrl-C to cancel)
This is real output, captured in zsh with default settings on the protected
main branch. (In bash the banner starts after a blank line. Off a protected branch the challenge is not escalated, and the banner
also tells you how to stop being asked: shellfirm config ignore <id>; that
hint is left out when the context raised the challenge.) The
exact output depends on the command, your settings and where you run it. The
challenge is a small math problem, pressing Enter, or typing yes: the higher
the severity and the riskier the context (protected branch, SSH, root,
production cluster), the stricter the challenge. Commands on a deny list are
blocked outright. Prefer no math? See Accessibility.
- 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 withshellfirm check --list --all; the generatedcheck catalogue has the exact, current count. - Shell hooks for 8 shells — Bash, Zsh, Fish, Elvish and Xonsh are tested end to end; Nushell is tested with a limitation (seeShell support ); the PowerShell and Oils hooks are shipped butnot yet tested end to end.
shellfirm statusshows which shells have the hook installed. - Context-aware escalation — harder challenges when connected via SSH, running as root, on protected git branches, or in production Kubernetes clusters
- Safer alternatives — many checks suggest a safer command (e.g.
--force-with-lease) - Severity levels with configurable thresholds (
Critical,High,Medium,Low,Info) - Team and org policies — commit a
.shellfirm.yamlwith 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. Seedocs/team-policies.md . - 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. Seedocs/audit.md . - 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 saysmore than 120,000 files (stopped counting), a step it could not start saysskipped, no time left. When it takes longer than 200 ms a transientshellfirm: checking impact… (Ctrl-C cancels)line shows; Ctrl-C there cancels the command and records it in the audit log - 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_severitystill asks - Feedback after you confirm — a passed challenge prints
✓ approved — running: <command>(dim, on the terminal only), so a slow command such ashelm uninstalldoes not look hung. Turn it off withshellfirm config approved-notice off - MCP server and agent hooks — use shellfirm as a guardrail for Claude Code, Cursor and other agents
- LLM analysis (experimental) — configured with
shellfirm config llm; used only to review AI-agent commands whenagent.llm_reviewis enabled
One command sets up both automatic safety (hooks) and on-demand analysis (MCP):
shellfirm connect claude-code
This adds:
- Hooks — every Bash command is checked before execution; risky commands are blocked
- MCP — Claude can call shellfirm tools to explain risks and suggest alternatives
Safe commands get no answer from the hook, so Claude Code's own permission
rules decide whether it asks you (the shellfirm 0.3.10 hook approved them
outright). After upgrading from 0.3.10, re-run shellfirm connect claude-code; see Upgrading from 0.3.10.
| Tool | Description |
|---|---|
check_command |
Check if a command is risky — returns severity, matched rules, and alternatives |
suggest_alternative |
Get safer replacement commands |
explain_risk |
Detailed explanation of why a command is dangerous |
get_policy |
Read the active shellfirm configuration |
Upgrading from 0.3.10? Upgrading the package only replaces the binary. Read Upgrading from 0.3.10: 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).
npm install -g @shellfirm/cli
brew tap kaplanelad/tap && brew install shellfirm
cargo install shellfirm
For prebuilt binaries through Cargo's binstall workflow:
cargo binstall shellfirm
Or download the binary from the releases page.
1. Install the shell hook:
shellfirm init # installs the hook for your current shell, then offers the others [y/N]
shellfirm init zsh # or only for one shell
shellfirm init --all # every supported shell
shellfirm init --dry-run # preview what would change
shellfirm init --yes # no questions (scripts, provisioning); sets the recommended Balanced level
shellfirm init asks you to confirm, then asks two questions: the challenge
type (Math, Enter or Yes) and the protection level.
2. Restart your shell (exec zsh, exec bash, … or open a new terminal).
3. Check it:
shellfirm status # shows hooks, settings, policy and thresholds
shellfirm doctor # checks that a new shell really routes commands through shellfirm
git reset --hard # should trigger shellfirm
| Shell | Status |
|---|---|
| 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. |
| 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. |
PowerShell, Oils ( osh ) |
Hooks are shipped but not yet tested end to end. Please report what works. |
The shell hooks check what you type at the interactive prompt. AI agents
should use the agent hooks (shellfirm connect), not the shell hook.
What the hook sees. The hooks expand what they can before checking:
aliases (including chains such as del → rmrf → rm -rf), history
references (!!, !$), and the body of a small shell function you call
(nuke() { rm -rf "$@"; }; nuke build), so the banner's Command: line
shows what will really run. In bash, zsh and fish every function a line
calls is followed, however deep, with the call's arguments (also after
shift), within a budget of 64 functions / 64 KB (16 KB for one body);
past it the line gets a Medium shell:function_depth_limit challenge
instead of being skipped. A function over 16 KB that was loaded from a file
(nvm, sdkman, conda) is treated like a script you run: its body is not read,
so nvm use 18 is not prompted, and neither is a call whose danger is only in
the body (bigclean ~ with rm -rf "$1" inside); a command it is asked to run
(nvm exec 18 rm -rf ~/data) is still checked. One typed or pasted into the
session gets the Medium challenge. Nushell follows aliases and def s, and
xonsh alias chains up to 10;
elvish fn functions are not expanded. Limits: the contents of scripts you
run (./cleanup.sh) are not inspected, and commands built at run time
($CMD, eval "$(…)" output) can only be checked as far as they are
visible on the line. shellfirm is a guard against mistakes, not a sandbox.
bash fc. bash turns the fc builtin off and routes command fc
through command_not_found_handle to the checked fc, so it runs in a
subshell; builtin fc works only on a line typed at the prompt.
After a blocked or cancelled line. In bash, zsh and fish, $?/$status
after a line shellfirm blocked is still the previous command's status: the
line never ran, and a key binding cannot set it. nushell sets
$env.LAST_EXIT_CODE = 1. The line is kept in the editor instead (Ctrl-U
clears it). Scripts and CI should use
shellfirm check --exit-code (exit 10 / 11) or shellfirm pre-command
(exit 65 / 77), not $? of an interactive prompt.
shellfirm check -c 'git reset --hard' # matched check ids, severity, alternatives
shellfirm check -c 'git reset --hard' --format json
The 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
(AI agent (hooks, MCP): deny), so a command you can run after a math problem
is not mistaken for one that is blocked for you. In CI, --strict also exits
78 when your settings, custom checks or a policy have warnings.
shellfirm has no "allow once" switch: to run a flagged command, solve the challenge. To stop being asked about a check permanently, ignore its id:
shellfirm config ignore git:reset # never prompt for this check
shellfirm config ignore --remove git:reset # undo
shellfirm config deny git:force_push # always block this check
shellfirm config severity High # only prompt for High and Critical
Unknown ids are rejected (with suggestions), so a typo cannot leave you
silently unprotected. Checks required by a project's .shellfirm.yaml cannot
be ignored or disabled for that project (shellfirm policy show lists them).
shellfirm config # interactive editor (TUI)
shellfirm config show # current settings
shellfirm config edit # open settings.yaml in $EDITOR (invalid edits are rejected)
shellfirm config reset # back up the current file, then start over
Settings live in ~/.config/shellfirm/settings.yaml (on macOS:
~/Library/Application Support/shellfirm/settings.yaml). An invalid settings
file is reported with its line number and is never overwritten; the shell hook
keeps working with safe defaults for the invalid values. See
docs/config.md.
--log <level> (or SHELLFIRM_LOG=debug) prints diagnostics to stderr.
RUST_LOG is not used.
No math challenges. If mental arithmetic is hard for you (dyscalculia, a
screen reader, fatigue), use these three settings. Medium (and, if you
prompt for them, Low and Info) commands then need only Enter, and everything
that would have asked for math asks you to type yes instead, including when
SSH, root or a protected branch raises the risk:
shellfirm config challenge Enter
shellfirm config escalation severity --critical Yes --high Yes --medium Enter --low Enter --info Enter
shellfirm config context escalation --elevated Yes --critical Yes
Check the result with shellfirm pre-command -t -c 'git push -f origin main'
(it prints the effective challenge). A project's .shellfirm.yaml or a
per-group or per-check override can still ask for Math. shellfirm policy show
and shellfirm config show list them.
Plain output. The challenge is linear text with one labelled fact per line,
and severity is written as a word. NO_COLOR=1 (any non-empty value; an
empty NO_COLOR= keeps colour), CLICOLOR=0 and a monochrome terminal
(TERM=dumb, vt100, xterm-mono, …) turn colour off everywhere: the
challenge, the CLI and the settings editor. CLICOLOR_FORCE=1 forces it on.
With a non-UTF-8 locale (LC_ALL=C), status, config show, policy show
and audit show print ASCII instead of —, → and ✓. There are no
timeouts, and retries are unlimited.
Settings editor (shellfirm config). Everything is reachable by keyboard
(? shows the keys). The active tab is shown as [Name] and the selected row
is marked with >, so focus is visible without colour. With NO_COLOR,
highlights use reverse video. With a non-UTF-8 locale (for example
LC_ALL=C), symbols fall back to ASCII. Text fields place the terminal cursor
at the insertion point. In windows smaller than 80x24, tabs scroll to the
selected row (the minimum is 60x16). Under TERM=dumb the editor does not
start; use shellfirm config show and the shellfirm config <subcommand>
commands instead.
shellfirm 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:
- Reverse shells: only the common spellings are recognised; other spellings are not.
- Encoded or downloaded text that is decoded and piped or passed straight into a shell or interpreter is not always followed.
- Sending secrets (keys, tokens, credential files) to another host with
curl,awsorghis not detected in general. - Reading a private key or credentials file is flagged (Medium), also with
Claude Code's
Readtool, but reading a.envfile and listing~/.sshare not. Add acustom check if you want to be asked before an agent reads them, for exampletest: '^(?:cat|less|head|tail|bat)\s+(?:\S+\s+)*?\S*\.env(?:\.(?:local|development|production|test))?(?:\s|$)'. - 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 like printing one; approve it. Checks cannot yet be limited to AI agents:config ignoreturns a check off for the shell and the agents together. - A destructive call made through an API with
curl(a GraphQL mutation that deletes a volume, aDELETErequest to a cloud API) is not recognised. - Lock-out commands beyond locking the root account (locking or removing other users, firewall rules that drop the SSH port) are not checked.
git -cwith a program value (core.pager,core.editor) is Medium, and-c diff.external=/-c credential.helper=are not matched; neither aregit config include.path …andgit config init.templateDir …, which make later git commands run another file's settings or hooks.- 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 when they come from a file or another program. - 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 agent is denied). Run the two commands one at a time, or approve it.
Nushell built-in commands are not stopped (see Shell support). Report a missing case as an issue; the check catalogue lists what is covered.
shellfirm init --uninstall # remove the hooks from your shell rc files
shellfirm connect claude-code --uninstall # if you connected an AI tool
shellfirm status # confirm: no shell shows "installed"
status reads your rc files, so it shows "not installed" right away; a shell
that already loaded the hook keeps it until you restart that shell.
If you added the hook by hand (eval "$(shellfirm init bash)" and similar),
delete that line from your rc file. Restart open shells: a running shell keeps
the hook until it exits.
Then remove the binary the way you installed it (npm uninstall -g @shellfirm/cli,
brew uninstall shellfirm, or cargo uninstall shellfirm) and, if you no
longer need them, delete your settings, custom checks and audit log:
rm -rf ~/.config/shellfirm (macOS: ~/Library/Application Support/shellfirm).
- Configuration — settings file, CLI, custom checks
- Team policies —
.shellfirm.yamlproject-level rules - Audit trail — record format, filters, rotation
- Check catalogue — every built-in check, by group
- Adding built-in patterns — contributing new checks
More guides are on shellfirm.vercel.app.
Contributions are welcome. CONTRIBUTING.md covers the build, the tests and the checklist for a pull request, and docs/add-new-patterns.md walks through adding a check. Found a bypass? Please report it privately; see SECURITY.md.