Run Python CLI tools safely in Docker containers — as easy as pipx
Project description
safe-whale
Run Python command-line tools in hardened container images, with a small Tkinter UI for discovery, profiles, launch wrappers, history, and cleanup.
safe-whale is meant to feel like pipx where that vocabulary fits, but the isolation boundary is a container image
rather than a virtual environment. Pick a PyPI CLI tool, inspect what kind of tool it is, build it once, then run it
safely or install a wrapper command.
Status
- Catalog search with usage patterns for one-shot CLIs, wrapper-first tools, pipe filters, and TUI apps.
- A
pipx-shaped CLI for run, install, list, rebuild, uninstall, cleanup, diagnostics, and wrapper PATH guidance. - PyPI metadata enrichment and local metadata caching.
- Saved profiles with stale-wrapper detection.
- pipx-style entry-point discovery: every
console_scripts/gui_scriptscommand a package exposes gets its own wrapper (probed from inside the built image withimportlib.metadata). .cmd+.ps1(Windows) or executable shell wrapper generation, one per discovered command.- Interactive
installsetup (when run in a terminal): prompts for the tool's usage pattern and makes the read-only vs. writable filesystem trade-off explicit. Use--yes/--no-inputto accept defaults non-interactively. - Managed cleanup for safe-whale-tracked images, Dockerfiles, and wrappers.
- Docker-first execution. Podman/nerdctl/finch are detected, but Docker is the most exercised path.
- Browser-hosted PyScript and direct Pyodide experiments. The Browser Python tab auto-loads a pure-Python package demo so package installation is visible on first run.
Quick Start
Install dependencies and launch the app:
uv sync
uv run safe-whale
The CLI can also be used directly. It intentionally mirrors the pipx command vocabulary where that maps to
container-backed tools:
uv run safe-whale run --dry-run ruff -- --version
uv run safe-whale install ruff --wrapper-dir .\.tmp\bin --skip-build
uv run safe-whale list
uv run safe-whale uninstall ruff --dry-run
When installed from PyPI, use the console script directly:
safe-whale --help
safe-whale run ruff -- --version
safe-whale install ruff
install builds the image, then discovers every command the package ships and installs a wrapper for each — e.g.
safe-whale install httpie produces both http and https. In a terminal it first asks for the tool's usage pattern
and whether to run with a read-only root filesystem; pass --yes (or --no-input) to skip the prompts and accept
defaults plus any flags you supplied.
Global options work before or after subcommands:
--dry-runshows Dockerfiles, build commands, run commands, profile changes, and cleanup actions without performing them.--log-level DEBUG|INFO|WARNING|ERROR|CRITICALcontrols logging.-v/--verboseis a shorthand for more detailed logging.--engine docker|podman|nerdctl|finchselects the container engine.--no-historyskips run history.
safe-whale-specific runtime options include --entrypoint, --apt, --interaction, --stdin-file, --mount,
--memory, --cpus, --network / --block-network, --read-only / --writable, --non-root / --root,
--cap-drop-all / --keep-caps, --tmpfs-tmp / --no-tmpfs-tmp, and --limit-pids / --no-limit-pids.
Other useful commands:
uv run safe-whale environment
uv run safe-whale ensurepath
uv run safe-whale cleanup
uv run safe-whale list --catalog --query ruff
uv run safe-whale install-all pipx-list.json
Useful development checks:
uv run pytest -q
uv run ruff check safe_whale tests
uv run mypy --hide-error-context safe_whale tests
In the Codex desktop sandbox on Windows, use the workspace-local cache/temp recipe in spec/codex.md.
Host Runtime Preliminaries
You need at least one runtime. Docker is the recommended default today.
| Host | Docker | Podman | Browser Python |
|---|---|---|---|
| Windows | Install Docker Desktop, enable WSL 2, verify with docker info. |
Install Podman Desktop/Podman CLI, then podman machine init and podman machine start. Avoid making Podman mimic Docker while Docker Desktop is also active unless you know which socket your tools use. |
PyScript/Pyodide run in the browser and do not need Node.js. |
| macOS | Install Docker Desktop and verify docker info. |
Install Podman, then podman machine init and podman machine start. |
PyScript/Pyodide run in the browser. |
| Linux | Install Docker Engine from Docker’s distro docs, or Docker Desktop if you prefer. Verify docker info. |
Install podman from your distro packages and verify podman info. |
PyScript/Pyodide run in the browser. |
Long version: docs/hosts.md.
Usage Patterns
Catalog entries carry a usage pattern so the UI can suggest the right action:
single_run_cli: works well in the in-app output panel.wrapper_cli: best installed as a shell/editor wrapper.pipe_filter: best as a wrapper, with optional file-based sample runs.tui_terminal: needs a real terminal; curses/full-screen apps cannot render correctly in the output panel.- Browser Pyodide/PyScript experiments: run in a generated browser app, not in the Tkinter output panel and not in Docker. They are useful for browser-compatible Python, not arbitrary PyPI CLI tools.
pipx Compatibility
safe-whale borrows pipx command names for familiar workflows, but it does not create per-tool virtual environments.
The main difference is:
| pipx | safe-whale |
|---|---|
| Installs apps into managed virtual environments. | Builds apps into managed container images. |
| Wrappers call venv entry points. | Wrappers run hardened containers. |
| Uses pip inside managed venvs. | Uses pip during image build, then runs the image. |
Implemented container-backed commands include run, install, install-all, list, profiles, uninstall,
uninstall-all, reinstall, reinstall-all, upgrade, upgrade-all, cleanup, environment, ensurepath,
completions, and help.
The commands inject, uninject, pin, unpin, runpip, interpreter, and upgrade-shared are recognized for
compatibility, but currently return an explicit unsupported-operation message because they do not map cleanly to the
container model yet.
Long version: docs/usage/pipx-compatibility.md.
Safety Model
safe-whale builds a Dockerfile first, then applies runtime hardening when running the already-built image:
- read-only root filesystem
- non-root UID
- no-new-privileges
- capability drop
- PID, CPU, and memory limits
- optional network blocking
- optional bind mount and stdin file for tools that need local input
Cleanup only targets assets safe-whale created or explicitly tracked.
Project Docs
- docs/index.md: MkDocs / Read the Docs home page.
- docs/installation.md: install, source checkout, and wrapper PATH setup.
- docs/usage/cli.md: current CLI command reference.
- docs/usage/pipx-compatibility.md: what matches pipx and what differs.
- docs/hosts.md: Docker, Podman, and Browser Python setup notes by host OS.
- spec/spec_v2.md: v2 product specification.
- spec/learnings.md: container/runtime lessons learned during development.
- spec/codex.md: local Codex sandbox notes.
Published documentation is configured through mkdocs.yml and .readthedocs.yaml.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file safe_whale-0.1.0.tar.gz.
File metadata
- Download URL: safe_whale-0.1.0.tar.gz
- Upload date:
- Size: 64.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d59dbf9fbcdfbb12a95707e89f14c28dff8110e689f7387fd57bd7955bcfe4eb
|
|
| MD5 |
e5d8a3e000c0f79188132df7c252ac09
|
|
| BLAKE2b-256 |
a3581258df650f419cda9d3148480752762ba9ce7d5c2901f851a7b9911c23ef
|
Provenance
The following attestation bundles were made for safe_whale-0.1.0.tar.gz:
Publisher:
publish_to_pypi.yml on matthewdeanmartin/safe_whale
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
safe_whale-0.1.0.tar.gz -
Subject digest:
d59dbf9fbcdfbb12a95707e89f14c28dff8110e689f7387fd57bd7955bcfe4eb - Sigstore transparency entry: 1676906926
- Sigstore integration time:
-
Permalink:
matthewdeanmartin/safe_whale@4525bb4ee65117bf306f747867906a25afeac271 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/matthewdeanmartin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish_to_pypi.yml@4525bb4ee65117bf306f747867906a25afeac271 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file safe_whale-0.1.0-py3-none-any.whl.
File metadata
- Download URL: safe_whale-0.1.0-py3-none-any.whl
- Upload date:
- Size: 60.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57cf623f8190a6edf9f7c46c89323939e4bda29e780356c29ac381c8e0c881c6
|
|
| MD5 |
5fe6dfe43411e38656dc18dd379a71e7
|
|
| BLAKE2b-256 |
da0ed2ba3a09215b0a1fd34f5c08085a15af588b6122494dc9da4e96d58221d6
|
Provenance
The following attestation bundles were made for safe_whale-0.1.0-py3-none-any.whl:
Publisher:
publish_to_pypi.yml on matthewdeanmartin/safe_whale
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
safe_whale-0.1.0-py3-none-any.whl -
Subject digest:
57cf623f8190a6edf9f7c46c89323939e4bda29e780356c29ac381c8e0c881c6 - Sigstore transparency entry: 1676906936
- Sigstore integration time:
-
Permalink:
matthewdeanmartin/safe_whale@4525bb4ee65117bf306f747867906a25afeac271 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/matthewdeanmartin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish_to_pypi.yml@4525bb4ee65117bf306f747867906a25afeac271 -
Trigger Event:
workflow_dispatch
-
Statement type: