from fastcore.test import test_fail,test_eqCore API
Introduction
safecmd.core provides a safe execution layer for shell commands. It’s designed for situations where you need to run bash commands from untrusted sources—such as LLM-generated commands—while ensuring they can’t modify your system in dangerous ways.
The module builds on top of safecmd.bashxtract (which parses bash into an AST and extracts commands) to validate commands against an allowlist before execution. The key insight is that rather than trying to blacklist dangerous commands (which is error-prone), we whitelist a generous set of read-only and easily-reverted commands that are safe to run.
The core workflow is:
- Parse the bash command string using
extract_commands()from bashxtract - Check each extracted command against
ok_cmds(the allowlist). Commands inside substitutions ($(...)), subshells, pipelines, etc are extracted recursively, so nested commands are also validated. - Check that output redirects only write to allowed destinations (default:
./and/tmp) - If everything passes, execute the command and return the result
This approach handles complex bash syntax correctly—pipelines, command substitutions, subshells, and more—because it uses a proper bash parser rather than regex or string splitting.
The allowlist (ok_cmds) uses prefix matching to determine if a command is permitted. A simple entry like 'ls' matches any command starting with ls—so ls, ls -la, and ls /home/user are all allowed. A multi-word entry like 'git status' only matches commands that start with both those words—so git status and git status --short are allowed, but git push is not.
This prefix approach lets you be precise about which subcommands are safe. For instance, you might allow git log, git status, and git diff (all read-only) while blocking git push and git reset (which modify state).
Some commands are mostly safe but have a few dangerous flags. For these cases, you can specify a denied list of flags that will cause the command to be rejected. For example, find allows searching but blocks -delete which would remove files.
Some flags take arguments that themselves need validation. Exec flags (like find -exec) have a next argument that’s a command—this command is parsed and validated recursively. So find . -exec ls passes (since ls is allowed) but find . -exec rm fails. Dest flags (like curl -o) have a next argument that’s an output destination—this is validated against ok_dests. So curl -o /tmp/file url passes but curl -o /etc/passwd url fails.
Some commands take commands or destinations as positional arguments rather than flags. For example, env runs whatever command is its first positional arg, and cp/mv write to their last arg. These use exec_pos and dest_pos—sets of positional indices (0-based, after the command name) where the argument is a command or destination to validate. Negative indices work as in Python (-1 for last arg). In the config format, these use $ prefix: env:exec=$0 or cp:dest=$-1.
Output redirects (>, >>, etc.) are also validated. By default, redirects can only write to the current directory (./) or /tmp. Bare relative paths like file.txt are normalized to ./file.txt before matching. You can customize allowed destinations via ok_dests.
The first time this module is used a config file (config.ini) is created with the default configuration. The file location follows the XDG Base Directory spec via xdg_config_home():
- Linux:
~/.config/safecmd/config.ini - macOS:
~/Library/Application Support/safecmd/config.ini - Windows:
%LOCALAPPDATA%\safecmd\config.ini(typicallyC:\Users\<username>\AppData\Local\safecmd\config.ini)
This file can be edited to change configuration.
How to use
The simplest way to use safecmd is to call safe_run() with a bash command string. This function validates the command against the built-in allowlist and executes it if safe, returning the combined stdout/stderr output as a string. If the command fails, it raises an IOError. If the command or destinations aren’t allowed, it raises either DisallowedCmd or DisallowedDest.
For example: safe_run('ls -la | grep py') will execute and return the filtered directory listing, while safe_run('rm -rf /') will raise a DisallowedCmd exception before anything dangerous happens.
The module comes with a predefined set of safe commands. This includes common read-only utilities like cat, grep, ls, diff, builtins like cd, export, [, and true, as well as safe git subcommands like git log, git status, and git diff. Commands like find have exec flags configured so that find . -exec ls passes but find . -exec rm fails. Commands like curl have dest flags so curl -o /tmp/file passes but writing to disallowed paths fails.
Output redirects are allowed but only to permitted destinations. By default, commands can write to the current directory (./) and /tmp. You can customize this by passing a dests parameter to safe_run(). For example, safe_run(cmd, add_dests='~/') would also allow writing to the home directory.
API
Helpers
run
def run(
cmd, ignore_ex:bool=False, split:bool=False
):
Run cmd in shell; return stdout (+ stderr if any); raise IOError on failure
Executes a shell command and returns its combined stdout/stderr output. If ignore_ex=True, returns a tuple of (returncode, output) instead of raising on failure. This is the low-level execution function—it doesn’t do any safety checking.
test_eq(run('echo hello'), 'hello')
test_eq(run('echo out; echo err >&2'), 'out\nerr')
test_eq(run('exit 1', ignore_ex=True), (1, ''))
test_eq(run('echo fail >&2; exit 1', ignore_ex=True), (1,'fail'))
test_fail(lambda: run('exit 1'))Command Specifications
CmdSpec
def CmdSpec(
name, # the command (str, will be split into tuple)
denied:NoneType=None, # if set, these flags blocked
exec_flags:NoneType=None, # flags whose next arg is a command to validate
dest_flags:NoneType=None, # flags whose next arg is a destination to validate
exec_pos:NoneType=None, # positional arg indices (0-based) that are commands to validate
dest_pos:NoneType=None, # positional arg indices (0-based) that are destinations to validate
):
Base class for objects needing a basic __repr__
CmdSpec represents an allowed command with optional denied flags, exec flags, dest flags, and positional exec/dest indices. The name is stored as a tuple for prefix matching—so CmdSpec('git log') matches git log, git log --oneline, etc. The denied set contains flags that will cause the command to be rejected. The exec_flags/dest_flags sets contain flags whose next argument is a command/destination to validate. The exec_pos/dest_pos sets contain positional argument indices (0-based, after the command name) that are commands/destinations—negative indices work as in Python (e.g., -1 for last arg).
find = CmdSpec('find', denied=['-exec', '-delete'])
findfind !{'-delete', '-exec'}
assert find(['find', '.', '-name', '*.py'])
assert not find(['find', '.', '-exec', 'rm'])
assert not find(['ls', '-la'])
# Combined short flags should be caught
tar = CmdSpec('tar', denied=['-I', '--to-command'])
assert tar(['tar', '-xvf', 'file.tar']) # allowed
assert not tar(['tar', '-I', 'zstd']) # exact match blocked
assert not tar(['tar', '-xvfI', 'zstd']) # combined flag blocked
assert not tar(['tar', '--to-command=cat']) # long flag still worksThe from_str classmethod provides a compact string syntax for creating CmdSpec objects. The format is command:-flag1|-flag2:exec=-exec|$0:dest=-o|$-1 where colons separate sections:
- First section: command name (can be multi-word like
git log) - Denied flags section:
|-separated flags to block (e.g.,-delete|-ok) - Exec section:
exec=prefix, then|-separated values—flags (like-exec) whose next arg is a command, or$Npositional indices (like$0) where the arg is a command - Dest section:
dest=prefix, then|-separated values—flags (like-o) whose next arg is a destination, or$Npositional indices (like$-1) where the arg is a destination
The $ prefix echoes bash positional parameter syntax. $0 is the first arg after the command name, $-1 is the last arg (Python-style negative indexing).
For example, CmdSpec.from_str('find:-delete:exec=-exec|-execdir') creates a spec that allows find, blocks -delete, and validates the command after -exec or -execdir. CmdSpec.from_str('cp:dest=$-1') validates cp’s last arg as a destination. CmdSpec.from_str('env:exec=$0') validates env’s first arg as a command. If no special flags are needed, just pass the command name: CmdSpec.from_str('cat').
test_eq(CmdSpec.from_str('cat'), CmdSpec('cat'))
test_eq(CmdSpec.from_str('find:-delete:exec=-exec|-execdir'), CmdSpec('find', denied=['-delete'], exec_flags=['-exec', '-execdir']))
test_eq(CmdSpec.from_str('curl:dest=-o|--output'), CmdSpec('curl', dest_flags=['-o', '--output']))
test_eq(CmdSpec.from_str('git log'), CmdSpec('git log'))
test_eq(CmdSpec.from_str('env:exec=$0'), CmdSpec('env', exec_pos=[0]))
test_eq(CmdSpec.from_str('cp:dest=$-1'), CmdSpec('cp', dest_pos=[-1]))
test_eq(CmdSpec.from_str('tee:dest=$0'), CmdSpec('tee', dest_pos=[0]))
test_eq(CmdSpec.from_str('curl:dest=-o|$-1'), CmdSpec('curl', dest_flags=['-o'], dest_pos=[-1]))Default Allowlists
In the default configuration, ok_dests specifies where output redirects can write (default: ./, /tmp). ok_cmds contains a generous set of read-only commands plus some safe git operations. Note that find blocks -delete but allows -exec with validation of the command argument. Commands like env and xargs use exec=$0 to validate their first positional arg as a command. Commands like cp, mv, tee, and ex use dest=$-1 or dest=$0 to validate their destination arg:
Exported source
default_cfg = '''[DEFAULT]
ok_dests = ./, /tmp
ok_cmds = cat, head, tail, less, more, bat
# Directory listing
ls, tree, locate
# Search
grep, rg, ag, ack, fgrep, egrep
# Text processing
cut, sort, uniq, wc, tr, column
# File info
file, stat, du, df, which, whereis, type
# Comparison
diff, cmp, comm
# Archives
unzip, gunzip, bunzip2, unrar
# Network
ping, dig, nslookup, host
# System info
date, cal, uptime, whoami, hostname, uname, printenv
# Utilities
echo, printf, yes, seq, basename, dirname, realpath
# Git (read-only)
git log, git show, git diff, git status, git branch, git tag, git remote,
git stash list, git blame, git shortlog, git describe, git rev-parse,
git ls-files, git ls-tree, git cat-file, git config --get, git config --list
# Git (workspace)
git fetch, git add, git commit, git switch, git checkout
# gh
gh repo view, gh issue list, gh issue view, gh pr list, gh pr view, gh pr status, gh pr checks, gh pr diff
gh release list, gh release view, gh run list, gh run view, gh workflow list, gh workflow view
gh auth status, gh gist list, gh gist view, gh browse, gh search
# nbdev
nbdev_export, nbdev_clean
# npm (read-only)
npm list, npm ls, npm outdated, npm view, npm info, npm why, npm audit, npm config list, npm config get, npm search, npm pack
# yarn (read-only)
yarn list, yarn outdated, yarn why, yarn info, yarn config list, yarn config get
# pnpm (read-only)
pnpm list, pnpm ls, pnpm outdated, pnpm why, pnpm config list, pnpm config get
# bun (read-only)
bun pm ls, bun pm hash
# js install
npm install, yarn install, pnpm install, bun install
# Modern Unix (read-only)
bat, eza, exa, fd, fzf, dust, duf, tldr, zoxide, httpie, http, jq, yq
# Docker (read-only)
docker ps, docker images, docker logs, docker inspect, docker stats, docker top, docker diff, docker history, docker version, docker info
# Docker (workspace - reversible)
docker pull, docker build
# AWS (read-only)
aws s3 ls, aws s3 cp, aws sts get-caller-identity, aws iam get-user, aws iam list-users
aws ec2 describe-instances, aws ec2 describe-vpcs, aws ec2 describe-security-groups
aws logs describe-log-groups, aws logs filter-log-events, aws logs get-log-events
aws lambda list-functions, aws lambda get-function
aws cloudformation describe-stacks, aws cloudformation list-stacks
aws rds describe-db-instances, aws dynamodb list-tables, aws dynamodb describe-table
aws sqs list-queues, aws sns list-topics
aws configure list, aws configure get
# GCloud (read-only)
gcloud config list, gcloud config get-value, gcloud auth list
gcloud projects list, gcloud projects describe
gcloud compute instances list, gcloud compute instances describe, gcloud compute zones list, gcloud compute regions list
gcloud container clusters list, gcloud container clusters describe
gcloud functions list, gcloud functions describe, gcloud functions logs read
gcloud run services list, gcloud run services describe
gcloud sql instances list, gcloud sql instances describe
gcloud storage ls, gcloud storage cat
gcloud logging read
# toolslm
folder2ctx, repo2ctx
# Positional exec/dest handling
env:exec=$0, xargs:exec=$0
tee:dest=$0, ex:dest=$0, cp:dest=$-1, mv:dest=$-1
# Exec/dest flag handling
find:-delete|-ok|-okdir:exec=-exec|-execdir
rg:--pre
tar:--use-compress-program|--transform|--checkpoint-action|--info-script|--new-volume-script:exec=--to-command|-I
curl:dest=-o|--output
# Builtins
cd, pwd, export, test, [, true, false
'''# cfg_path.unlink()If config.ini doesn’t exist, it’s created with the default configuration, in the file location following the XDG Base Directory spec.
parse_cfg
def parse_cfg(
cfg_str
):
Parse config string, return (ok_dests set, ok_cmds set of CmdSpecs)
The config is parsed into ok_dests and ok_cmds.
print(ok_dests)
list(ok_cmds)[:7]{'/tmp', '/Users/jhoward/aai-ws', '/Users/jhoward/git', './'}
[pwd,
gh auth status,
gcloud projects list,
gcloud run services list,
gh workflow list,
yarn info,
yarn config get]
first(o for o in ok_cmds if str(o).startswith('find'))find !{'-okdir', '-ok', '-delete'} exec={'-execdir', '-exec'}
Safe Execution
validate_cmd
def validate_cmd(
toks, cmds:NoneType=None
):
Check if toks matches an allowed command; returns False if denied flags present
validate_cmd checks whether a tokenized command matches any entry in the allowlist by calling each CmdSpec until one returns True.
assert validate_cmd(['ls', '-la'])
assert validate_cmd(['git', 'status'])
assert validate_cmd(['find', '.', '-name', '*.py'])
assert validate_cmd(['find', '.', '-exec', 'rm']) # -exec now handled by exec_flags, not denied
assert not validate_cmd(['find', '.', '-delete']) # -delete is still denied
assert not validate_cmd(['rm', '-rf', '/'])
assert not validate_cmd(['git', 'push'])DisallowedDest
def DisallowedDest(
dest
):
Not enough permissions.
DisallowedCmd
def DisallowedCmd(
cmd
):
Not enough permissions.
DisallowedError
def DisallowedError(
args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):
Not enough permissions.
validate_dest
def validate_dest(
dest, dests:NoneType=None
):
Check if dest (resolved to absolute) matches an allowed destination pattern
normalize_dest
def normalize_dest(
dest
):
Normalize destination to absolute path, expanding ~ and env vars
normalize_dest resolves paths to absolute, expanding ~ and environment variables (like $HOME) and normalizing .. components. This prevents path traversal attacks where ./.. or ./subdir/../../escape would otherwise match the ./ pattern. validate_dest checks if a resolved absolute path starts with any allowed pattern (also resolved to absolute).
cwd = os.getcwd()
home = os.path.expanduser('~')
parent = os.path.dirname(cwd)
# normalize_dest now returns absolute paths
test_eq(normalize_dest('file.txt'), f'{cwd}/file.txt')
test_eq(normalize_dest('./file.txt'), f'{cwd}/file.txt')
test_eq(normalize_dest('/tmp/file'), '/tmp/file')
test_eq(normalize_dest('../up.txt'), f'{parent}/up.txt')
test_eq(normalize_dest('~/home.txt'), f'{home}/home.txt')
test_eq(normalize_dest('$HOME/file'), f'{home}/file')
# With default ok_dests = {'./', '/tmp'}
assert validate_dest('file.txt') # /cwd/file.txt matches /cwd/
assert validate_dest('./subdir/f.txt') # /cwd/subdir/f.txt matches /cwd/
assert validate_dest('/tmp/test') # matches /tmp
assert not validate_dest('/etc/passwd') # no match
assert not validate_dest('../../../../up.txt') # resolves outside cwd - blocked!
assert not validate_dest('~/file') # ~/ not in defaultsvalidate checks a bash command string against the allowlists without executing it. This is useful for pre-validation (e.g., in hooks or UI) where you want to know if a command would be allowed before actually running it. It raises DisallowedCmd or DisallowedDest if validation fails.
validate
def validate(
cmd:str, # Bash command string to validate
cmds:NoneType=None, # Allowed commands set; defaults to ok_cmds
dests:NoneType=None, # Allowed destinations set; defaults to ok_dests
):
Validate cmd against allowlists; raises DisallowedCmd or DisallowedDest on failure
_build_flag_dicts extracts exec_flags, dest_flags, exec_pos, and dest_pos dicts from a set of CmdSpec objects. Each dict maps command names to their respective flag sets or positional index sets, which are then passed to extract_commands for recursive validation.
# Safe commands pass validation silently
validate('ls -la | grep py')
validate('git status && echo done')
validate('echo hi > file.txt') # allowed - writes to ./file.txt
validate('cat data > /tmp/out') # allowed - /tmp is ok
# Unsafe commands raise exceptions
test_fail(lambda: validate('rm -rf /'), exc=DisallowedCmd)
test_fail(lambda: validate('echo hi > /etc/badplace'), exc=DisallowedDest)
test_fail(lambda: validate('ls $(rm -rf /)'), exc=DisallowedCmd) # nested command caught
test_fail(lambda: validate('echo > ../../../../escape.txt'), exc=DisallowedDest) # parent dir not allowed
# Path traversal attacks - must be blocked
test_fail(lambda: validate('echo hi > ./../../../..'), exc=DisallowedDest) # escapes via ./..
test_fail(lambda: validate('echo hi > ./../../../../escape.txt'), exc=DisallowedDest) # escapes via ./../
test_fail(lambda: validate('echo hi > ./subdir/../../../../escape.txt'), exc=DisallowedDest) # nested escape
test_fail(lambda: validate('echo hi > /tmp/../bad.txt'), exc=DisallowedDest) # escape via /tmp/../
# Resolved paths that stay within allowed dirs should work
validate('echo hi > ./subdir/../file.txt') # resolves to ./file.txt, still in cwd# exec_flags: find -exec with allowed command passes, with disallowed command fails
validate('find . -exec ls')
test_fail(lambda: validate('find . -exec rm'), exc=DisallowedCmd)
# dest_flags: curl -o with allowed dest passes, with disallowed dest fails
validate('curl -o /tmp/out http://example.com')
test_fail(lambda: validate('curl -o /etc/passwd http://example.com'), exc=DisallowedDest)Positional exec/dest validation: env and xargs validate their first arg as a command, while cp, mv, tee, and ex validate their destination arg against ok_dests:
validate('env ls')
test_fail(lambda: validate('env rm'), exc=DisallowedCmd)
test_fail(lambda: validate('xargs rm'), exc=DisallowedCmd)
validate('cp src.txt ./dest.txt')
validate('mv old.txt /tmp/new.txt')
test_fail(lambda: validate('cp src.txt /etc/passwd'), exc=DisallowedDest)
test_fail(lambda: validate('mv old.txt /etc/shadow'), exc=DisallowedDest)
validate('tee /tmp/out.log')
validate('ex ./myfile.txt')
test_fail(lambda: validate('tee /etc/passwd'), exc=DisallowedDest)
test_fail(lambda: validate('ex /etc/shadow'), exc=DisallowedDest)cmds, dests = _eff_sets(add_cmds='wget', add_dests='~/Downloads', rm_cmds='cat', rm_dests='/tmp')
print('wget allowed:', any(s.name==('wget',) for s in cmds))
print('cat allowed:', any(s.name==('cat',) for s in cmds))
print('dests:', dests)wget allowed: True
cat allowed: False
dests: {'/Users/jhoward/aai-ws', '/Users/jhoward/git', './', '~/Downloads'}
safe_run
def safe_run(
cmd:str, # Bash command string to execute
cmds:str=None, # Allowed commands (comma-separated, config format); defaults to ok_cmds
dests:str=None, # Allowed destinations (comma-separated); defaults to ok_dests
add_cmds:str=None, # Temp add these commands
add_dests:str=None, # Temp add these destinations
rm_cmds:str=None, # Temp remove these commands
rm_dests:str=None, # Temp remove these destinations
ignore_ex:bool=False, # If True, return (returncode, output) instead of raising on error
split:bool=False, # If True, return stdout and stderr separately
)->str: # Combined stdout/stderr output
Run cmd in shell if all commands and destinations are in allowlists, else raise
safe_run is the main entry point. It parses the bash command, validates all extracted commands and redirect destinations against the allowlists, and only executes if everything passes. DisallowedCmd and DisallowedDest are raised for violations, giving clear error messages about what was blocked.
test_eq(safe_run('ls'), run('ls'))
test_eq(safe_run('echo hello | cat'), 'hello')
test_eq(safe_run('[ -f /etc/passwd ] && echo exists'), 'exists')
assert '00_bashxtract.ipynb' in safe_run('find . -exec ls \;')
# Redirects to allowed destinations work
safe_run('echo test > /tmp/safecmd_test_xyz')
safe_run('echo test > test_file_xyz.txt')
test_fail(lambda: safe_run(r'env rm -rf asdfff'), exc=DisallowedCmd)
test_fail(lambda: safe_run('echo hi > /badpath/file'), exc=DisallowedDest)
test_fail(lambda: safe_run('find . -exec sudo ls \;'), exc=DisallowedCmd)Pass ignore_ex=True to return a tuple of return_code,result instead of raising on error on command failed. (Permission failures still raise an error however.)
safe_run('cat /nonexistent_xyz123 2>&1', ignore_ex=True)(1, 'cat: /nonexistent_xyz123: No such file or directory')
Bash tools
In Solveit, any function with types and a docstring can be used as a tool. Instead of raising an exception, it’s best to return a success/error dict. The functions in this section wrap safe_run in this way, and provide documentation suitable for an LLM.
bash
def bash(
cmd:str, # Bash command string to execute - all shell features like pipes and subcommands are supported
rm_cmds:str=None, # Temp remove these commands from allow list
rm_dests:str=None, # Temp remove these destinations from allow list
):
Run a bash shell command line safely and return the concatencated stdout and stderr. cmd is parsed and all calls are checked against an allow-list. If the command is not allowed, STOP and inform the user of the command run and error details; so they can decide whether to whitelist it or run it themselves. The default allow-list includes most standard unix commands and git subcommands that do not change state or are easily reverted. All operators are supported. Output redirects are validated against allowed destinations (default: ./ and /tmp). rm_ params are comma-separated strs.
bash does not surface any parameters that could allow the LLM to add or change the allowed tool list.
bash('ls | head -2'){'success': '_quarto.yml\n00_bashxtract.ipynb'}
r = bash('ls | head -2', rm_cmds='head')
r['error']DisallowedCmd('head -2')
list(r)['error', 'allowed_cmds', 'allowed_dests', 'suggestion']
bash('sudo ls')['error']DisallowedCmd('sudo ls')
unsafe_bash
def unsafe_bash(
cmd:str, # Bash command string to execute - all shell features like pipes and subcommands are supported
cmds:str=None, # Allowed commands; defaults to ok_cmds; DO NOT USE without upfront user permission
dests:str=None, # Allowed destinations; defaults to ok_dests; DO NOT USE without upfront user permission
add_cmds:str=None, # Temp add these commands to allow list; DO NOT USE without upfront user permission
add_dests:str=None, # Temp add these destinations to allow list; DO NOT USE without upfront user permission
rm_cmds:str=None, # Temp remove these commands from allow list
rm_dests:str=None, # Temp remove these destinations from allow list
):
Run a bash shell command line safely and return the output. cmd is parsed and all calls are checked against an allow-list. If the command is not allowed, STOP and inform the user of the command run and error details; so they can decide whether to whitelist it or run it themselves. The default allow-list includes most standard unix commands and git subcommands that do not change state or are easily reverted. All operators are supported. Output redirects are validated against allowed destinations. cmds/dests and add_/rm_ params are comma-separated strs.
unsafe_bash is like bash but exposes parameters to modify the allowlists. The add_cmds and add_dests parameters let an LLM temporarily expand what’s allowed—use with caution, and only with explicit user permission. This is useful when you trust the LLM to make safe expansions in specific contexts.
rm_allowed_dests
def rm_allowed_dests(
dests
):
Remove comma-separated dests from the allow list
rm_allowed_cmds
def rm_allowed_cmds(
cmds:str
):
Remove comma-separated cmds from the allow list
add_allowed_dests
def add_allowed_dests(
dests
):
Add comma-separated dests to the allow list; (this can not be used as an LLM tool)
add_allowed_cmds
def add_allowed_cmds(
cmds
):
Add comma-separated cmds to the allow list; (this can not be used as an LLM tool)
These functions modify the global ok_cmds and ok_dests sets at runtime. add_allowed_cmds and add_allowed_dests expand the allowlist, while rm_allowed_cmds and rm_allowed_dests restrict it. The add_ functions are intentionally not exposed as LLM tools to prevent an LLM from expanding its own permissions.
rm_allowed_cmds('ls')bash('ls -l')['error']DisallowedCmd('ls -l')
ex
def ex(
path:str, # The file to run [`ex`](https://AnswerDotAI.github.io/safecmd/core.html#ex) on
cmds:str, # The commands to run (a 'heredoc' is used automatically, so embedded newlines work
):
Run ex commands on a file via bash. TIP: Great for in/dedent, join, g/pat/cmd, copy/cut/paste, etc. Remember: e.g 267,268j means “run j on each line in the range”, so it joins 267→268, then joins what’s now 268 (the old 269) too. - 267j wil just join 267 with the next line. NB: for inserting/deleting/replacing lines/strs, use the dedicated tools like str_replace, not ex, where possible.
ex is an example of the kind of targeted tool you can now provide to an LLM, allowing it to have access to all the power of vim, with the safety of path checking.
ex('styles.css', cmds='2p'){'success': ['margin-bottom: 1rem;', ':x']}
CLI
main
def main(
):
The CLI provides a simple command-line interface to safe_run.
Usage:
safecmd ls -laIf you have pipes etc, you’ll need to quote the whole command:
safecmd 'ls -la | grep py'The command and all its arguments are joined back into a single string and passed to safe_run, which validates against the allowlist before execution. If the command isn’t allowed, it shows an error and returns exit code of 1.
This lets you use safecmd as a drop-in replacement for running untrusted commands from scripts or other tools: anything not in the allowlist is blocked before execution.