Skip to content

Repository files navigation

shellsafe

Run shell commands safely using Python 3.14 template strings. Values can never turn into commands.

from shellsafe import run

message = get_user_input()          # "fix; rm -rf ~"
run(t"git commit -m {message}")
# argv: ["git", "commit", "-m", "fix; rm -rf ~"]
# one command; the scary text is just an argument

Why this package exists

Python 3.14 added template strings (PEP 750). Now Python keeps your fixed text and your values separate at the language level.

Shell commands are the first place people want to use this. That is because f-strings inside shell commands have caused real security bugs for ten years:

subprocess.run(f"git commit -m {message}", shell=True)
# if message = "fix; rm -rf ~"  ->  two commands run. The second one is bad.

Python planned to solve this officially (PEP 787), but that PEP is still deferred. It is not in the Python 3.15 release candidates. So today there is no standard way to run shell commands safely with templates. This package fills that gap.

Install

pip install shellsafe

Needs Python 3.14 or newer.

How to use

Run a command. Your values always stay one argument each. run() never invokes a shell, so injection is impossible on any platform:

from shellsafe import run

run(t"mkdir {path}")
run(t"docker build -t {tag} .", check=True, timeout=300)

Get the output as text:

from shellsafe import capture

res = capture(t"grep {pattern} {file}")
print(res.stdout, res.returncode)

Need pipes? Shell execution is disabled: shx() always raises, and any template containing |, >, &, or other shell metacharacters is refused before anything runs. Interpolated values could trigger command substitution inside existing quote contexts, so the shell route had to go. Split the pipeline into separate argv commands instead:

import subprocess

from shellsafe import capture

first = capture(t"cat README.md")
count = subprocess.run(["wc", "-l"], input=first.stdout, capture_output=True, text=True)
print(count.stdout.strip())

Want to see exactly what will run?

from shellsafe import plan

print(plan(t"git commit -m {message}"))
# argv: ["git","commit","-m","fix; rm -rf ~"]

Safety rules

Case What happens
Any value you pass becomes one argument, exactly as given
Value used as the program name error: program names must be written as fixed text
Templates with shell metacharacters error: shell execution is disabled; split into separate argv commands
RAW() misuse error: one value only, used as-is, no nesting

RAW(...) marks content you have already made safe by hand. It is loud and easy to find in code review, so trust is never hidden.

Find old dangerous patterns

Already have code using f-strings in shell commands? The scanner finds them:

shellsafe audit src/

It detects f-strings passed to os.system, subprocess.run(shell=True), and other shell executors. Get machine-readable output:

shellsafe audit src/ --json

Filter by severity:

shellsafe audit src/ --severity warning

Suppress known-safe findings inline:

# shellsafe: ignore AU001 reason: tested, value is constant
os.system(f"echo {safe_value}")

Suppress from the command line:

shellsafe audit src/ --ignore AU004

Limits

  • Shell execution is disabled on every platform: shx() always raises, and plan()/run()/capture() refuse templates containing shell metacharacters (pipes, redirections, &&, command substitution). Split pipelines into separate argv commands connected with explicit subprocess calls, as shown above.
  • Byte values are rejected. Decode them first.
  • We keep your command safe to build and run. Testing what your command does is still your job.

Needs

  • Python 3.14 or newer
  • Linux, macOS, Windows (plain argv commands on all three; shell execution is disabled everywhere)

More

License: MIT

About

Run shell commands safely using Python 3.14 template strings. Injection proof by construction.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages