Mino
Secure sandbox wrapper for AI coding agents using rootless Podman containers.
Wraps any command in isolated containers with temporary cloud credentials and SSH agent forwarding. Works with Claude Code, Aider, Cursor, or any CLI tool.
Why Mino?
AI coding agents are powerful but require significant system access. Mino provides defense-in-depth:
- Filesystem Isolation: Agent only sees your project directory, not
~/.ssh,~/.aws, or system files - Credential Scoping: Short-lived cloud tokens instead of permanent credentials
- Network Boundaries: Four network modes — bridge (default), host, none, or allowlisted egress via iptables — with built-in presets for common services
Features
- Rootless Containers: Podman containers with no root required (OrbStack VM on macOS, native on Linux)
- Temporary Credentials: Generates short-lived AWS/GCP/Azure tokens (1-12 hours)
- SSH Agent Forwarding: Git authentication without exposing private keys
- Persistent Caching: Content-addressed dependency caches survive session crashes
- Multi-Session: Run multiple isolated sandboxes in parallel
- Network Isolation: Bridge networking by default with interactive prompt on first run. Block all traffic, allowlist specific destinations, or use built-in presets (
dev,registries) - Zero Config: Works out of the box with sensible defaults
Requirements
- macOS: OrbStack installed (manages a lightweight Linux VM with Podman)
- Linux: Podman installed in rootless mode (no VM needed)
- Cloud CLIs (optional):
aws,gcloud,az,gh
Run mino setup to check and install prerequisites for your platform.
Installation
npm
Homebrew
From Source
Verify Installation
Quick Start
# Interactive shell in sandbox
# Run Claude Code in sandbox
# Run with AWS credentials
# Run with all cloud credentials
# Named session with specific project
# Use a different container image
# Use mino development images (with Claude Code pre-installed)
CLI Reference
Global Options
These options work with all commands:
| Option | Description |
|---|---|
-v, --verbose |
Enable verbose output |
-c, --config <PATH> |
Configuration file path (env: MINO_CONFIG) |
--no-local |
Skip local .mino.toml discovery |
Commands
mino run
Start a sandboxed session.
| Option | Description |
|---|---|
-n, --name <NAME> |
Session name (auto-generated if omitted) |
-p, --project <PATH> |
Project directory to mount (default: current dir) |
--image <IMAGE> |
Container image (default: fedora:43). Aliases: typescript/ts/node, rust/cargo, python/py, base |
--aws |
Include AWS credentials |
--gcp |
Include GCP credentials |
--azure |
Include Azure credentials |
--all-clouds |
Include all cloud credentials |
--github |
Include GitHub token (default: true) |
--ssh-agent |
Forward SSH agent (default: true) |
--layers <LAYERS> |
Composable layers (comma-separated, conflicts with --image) |
-e, --env <KEY=VALUE> |
Additional environment variable |
--volume <HOST:CONTAINER> |
Additional volume mount |
-d, --detach |
Run in background |
--no-cache |
Disable dependency caching |
--cache-fresh |
Force fresh cache (ignore existing) |
--network <MODE> |
Network mode: bridge (default), host, none |
--network-allow <RULES> |
Allowlisted destinations (host:port, comma-separated). Implies bridge + iptables |
--network-preset <PRESET> |
Network preset: dev, registries (conflicts with --network-allow) |
Layer precedence: --layers flag > --image flag > MINO_LAYERS env var > config container.layers > interactive selection > config container.image.
Set MINO_LAYERS=rust,typescript in your environment for non-interactive layer selection (CI, IDE plugins). When no layers or image are configured and the terminal is interactive, mino run prompts for layer selection with an option to save to config.
mino exec
Execute a command in a running session.
| Option | Description |
|---|---|
SESSION |
Session name (defaults to most recent running session) |
COMMAND |
Command to run (defaults to /bin/zsh) |
Examples:
mino list
List sessions.
| Option | Description |
|---|---|
-a, --all |
Show all sessions including stopped |
-f, --format <FORMAT> |
Output format: table, json, plain (default: table) |
mino stop
Stop a running session.
| Option | Description |
|---|---|
-f, --force |
Force stop without graceful shutdown |
mino logs
View session logs.
| Option | Description |
|---|---|
-f, --follow |
Follow log output (like tail -f) |
-l, --lines <N> |
Number of lines to show (default: 100, 0 = all) |
mino status
Check system health and dependencies.
mino setup
Install and configure prerequisites interactively.
| Option | Description |
|---|---|
-y, --yes |
Auto-approve all installation prompts |
--check |
Check prerequisites only, don't install |
--upgrade |
Upgrade existing dependencies to latest versions |
mino init
Initialize a project-local .mino.toml configuration file.
| Option | Description |
|---|---|
-f, --force |
Overwrite existing .mino.toml |
-p, --path <DIR> |
Target directory (default: current directory) |
mino cache
Manage dependency caches.
| Subcommand | Description |
|---|---|
list [-f FORMAT] |
List all cache volumes |
info [-p PATH] |
Show cache info for current/specified project |
gc [--days N] [--dry-run] |
Remove caches older than N days |
clear --volumes|--images|--all [-y] |
Clear cache volumes, composed images, or both |
mino config
Show or edit configuration.
| Subcommand | Description |
|---|---|
show |
Show current configuration (default) |
path |
Show configuration file path |
init [--force] |
Initialize default configuration |
set <KEY> <VALUE> |
Set a configuration value (e.g., vm.name myvm) |
mino completions
Generate shell completion scripts.
Supported shells: bash, zsh, fish, elvish, powershell.
Installation:
# Bash — write to completions directory
# Zsh — write to fpath directory
# Fish — write to completions directory
Alternatively, add eval "$(mino completions bash)" or eval "$(mino completions zsh)" to your shell's rc file.
Configuration
Configuration is stored at ~/.config/mino/config.toml:
[]
= false
= "text" # "text" or "json"
= true # Security events written to state dir
[]
= "mino"
= "fedora"
[]
= "fedora:43"
= "/workspace"
= "bridge"
# network_preset = "dev" # Preset allowlist: dev, registries
# network_allow = ["github.com:443"] # Implies bridge + iptables egress filtering
# env = { "MY_VAR" = "value" } # Additional env vars
# volumes = ["/host/path:/container/path"]
# layers = ["typescript", "rust"] # Composable language layers
[]
= false # Enable via config (equivalent to --aws)
= 3600 # Token lifetime (1-12 hours)
# role_arn = "arn:aws:iam::123456789012:role/MyRole"
# external_id = "my-external-id"
# profile = "default"
# region = "us-east-1"
[]
= false # Enable via config (equivalent to --gcp)
# project = "my-project"
# service_account = "sa@project.iam.gserviceaccount.com"
[]
= false # Enable via config (equivalent to --azure)
# subscription = "subscription-id"
# tenant = "tenant-id"
[]
= "github.com" # For GitHub Enterprise
[]
= "/bin/bash"
= 720 # Auto-cleanup stopped sessions (0 = disabled)
# default_project_dir = "/path/to/default/project"
[]
= true # Enable dependency caching
= 30 # Auto-remove caches older than N days
= 50 # Max total cache size before GC
Configuration Keys
Use mino config set <key> <value> to modify:
general.verbose
general.log_format
general.audit_log
vm.name
vm.distro
container.image
container.network
container.network_preset
container.workdir
container.network_allow
credentials.aws.enabled
credentials.aws.session_duration_secs
credentials.aws.role_arn
credentials.aws.profile
credentials.aws.region
credentials.gcp.enabled
credentials.gcp.project
credentials.azure.enabled
credentials.azure.subscription
credentials.azure.tenant
session.shell
session.auto_cleanup_hours
Dependency Caching
Mino automatically caches package manager dependencies using content-addressed volumes. If a session crashes, the cache persists and is reused on the next run.
How It Works
-
Lockfile Detection: On
mino run, scans for lockfiles:package-lock.json/npm-shrinkwrap.json-> npmyarn.lock-> yarnpnpm-lock.yaml-> pnpmCargo.lock-> cargorequirements.txt/Pipfile.lock-> pippoetry.lock-> poetryuv.lock-> uvgo.sum-> go
-
Cache Key:
sha256(lockfile_contents)[:12]- same lockfile = same cache -
Cache States:
State Mount When Miss read-write No cache exists, creating new Building read-write In progress or crashed (retryable) Complete read-only Finalized, immutable -
Environment Variables: Automatically configured:
npm_config_cache=/cache/npm CARGO_HOME=/cache/cargo PIP_CACHE_DIR=/cache/pip UV_CACHE_DIR=/cache/uv XDG_CACHE_HOME=/cache/xdg
Security
- Tamper-proof: Complete caches are mounted read-only
- Content-addressed: Changing dependencies = new hash = new cache
- Isolated: Each unique lockfile gets its own cache volume
Cache Management
# View caches for current project
# List all cache volumes
# Remove old caches (default: 30 days)
# Remove caches older than 7 days
# Clear everything
Network Isolation
Mino supports four network modes for container sessions:
Modes
| Mode | Flag | Behavior |
|---|---|---|
| Bridge | --network bridge (default) |
Standard bridge networking, isolated from host localhost |
| Host | --network host |
Full host networking, no restrictions |
| None | --network none |
No network access at all |
| Allowlist | --network-allow host:port,... |
Bridge + iptables egress filtering |
| Preset | --network-preset dev|registries |
Allowlist with built-in rules for common services |
Examples
# Default: bridge networking (isolated from host localhost)
# No network access (air-gapped)
# Allow only GitHub and npm registry
# Use dev preset (GitHub, npm, crates.io, PyPI, AI APIs)
# Use registries preset (package repos only, most restrictive)
# Full host networking (no isolation)
Allowlist Mode
When using --network-allow, Mino:
- Sets the container to bridge networking
- Adds
CAP_NET_ADMINcapability - Wraps your command with iptables rules that:
- DROP all outbound traffic (IPv4 + IPv6)
- ACCEPT loopback traffic
- ACCEPT established/related connections
- ACCEPT DNS (port 53, UDP + TCP)
- ACCEPT each allowlisted host:port
Presets
| Preset | Destinations | Use case |
|---|---|---|
dev |
github.com (443, 22), api.github.com, registry.npmjs.org, crates.io, static.crates.io, index.crates.io, pypi.org, files.pythonhosted.org, api.anthropic.com, api.openai.com | Dev with AI agents |
registries |
registry.npmjs.org, crates.io, static.crates.io, index.crates.io, pypi.org, files.pythonhosted.org | Package install only |
Configuration
Set default network allowlist in config:
[]
= "bridge" # default mode
# network_preset = "dev" # preset allowlist (conflicts with network_allow)
= ["github.com:443", "npmjs.org:443"] # implies bridge + iptables
Or via CLI: mino config set container.network_allow "github.com:443,npmjs.org:443"
Known Limitations
- DNS resolution at rule time: iptables resolves hostnames to IPs when rules are inserted. CDN hosts with rotating IPs may become unreachable during long sessions.
- iptables required: The container image must include iptables. Fedora 43 and mino-base include it by default.
- capsh required:
--network-allowand--network-presetmodes requirecapsh(fromlibcap) in the container image to dropCAP_NET_ADMINafter iptables setup. The mino-base image includes it.
Container Images
Mino uses a base image (mino-base) with a layer composition system for language toolchains.
| Alias | Behavior | Includes |
|---|---|---|
typescript, ts, node |
Layer composition from mino-base |
Node.js 22 LTS, pnpm, tsx, TypeScript, biome |
rust, cargo |
Layer composition from mino-base |
rustup, cargo, clippy, bacon, sccache |
python, py |
Layer composition from mino-base |
Python 3.13, uv, ruff, pytest |
base |
Pulls ghcr.io/dean0x/mino-base |
Claude Code, git, delta, ripgrep, zoxide |
Language aliases trigger layer composition at runtime — the toolchain is installed on top of mino-base using install.sh scripts. Layers can be composed together with --layers typescript,rust.
All images include: Claude Code CLI, git, gh CLI, delta (git diff), ripgrep, fd, bat, fzf, neovim, zsh, zoxide.
See images/README.md for full tool inventory and layer architecture.
Custom Layers
You can create custom layers to extend mino-base with any toolchain.
Layer Locations
| Location | Path | Scope |
|---|---|---|
| Project-local | .mino/layers/{name}/ |
Current project only |
| User-global | ~/.config/mino/layers/{name}/ |
All projects |
| Built-in | Bundled with mino | All projects |
Resolution order: project-local > user-global > built-in (first match wins). This lets you override built-in layers per-project or per-user.
Creating a Layer
Each layer needs two files: layer.toml (metadata) and install.sh (setup script).
layer.toml — declares environment variables, PATH extensions, and cache paths:
[]
= "go"
= "Go toolchain + tools"
= "1"
[]
= "/cache/go"
= "/cache/go/mod"
= "/cache/go/build"
[]
= ["/usr/local/go/bin", "/cache/go/bin"]
[]
= ["/cache/go"]
install.sh — runs as root on mino-base. Must be idempotent (safe to re-run):
#!/usr/bin/env bash
# Install Go (idempotent)
if ! ; then
|
fi
# Install tools
# Fix permissions
# Verify
Using Custom Layers
# Use by name (resolved from layer locations)
# Compose multiple layers
# Set via environment for CI
Overriding Built-in Layers
To customize a built-in layer, create a layer with the same name in your project or user config directory. Your version takes precedence:
.mino/layers/typescript/layer.toml # overrides built-in typescript
.mino/layers/typescript/install.sh
Architecture
macOS (via OrbStack)
macOS Host
|
+- mino CLI (Rust binary)
| - Validates environment (OrbStack, Podman)
| - Generates temp credentials (STS, gcloud, az)
| - Manages session lifecycle
|
+-> OrbStack VM (lightweight Linux, ~200MB)
|
+-> Podman rootless container
- Mounted: /workspace (project dir only)
- SSH agent socket forwarded
- Temp credentials as env vars
- NO access to: ~/.ssh, ~/, system dirs
Linux (native Podman)
Linux Host
|
+- mino CLI (Rust binary)
| - Validates environment (rootless Podman)
| - Generates temp credentials (STS, gcloud, az)
| - Manages session lifecycle
|
+-> Podman rootless container (no VM layer)
- Mounted: /workspace (project dir only)
- SSH agent socket forwarded
- Temp credentials as env vars
- NO access to: ~/.ssh, ~/, system dirs
Credential Strategy
| Service | Method | Lifetime |
|---|---|---|
| SSH/Git | Agent forwarding via socket | Session |
| GitHub | gh auth token |
Existing token |
| AWS | STS GetSessionToken/AssumeRole | 1-12 hours |
| GCP | gcloud auth print-access-token |
1 hour |
| Azure | az account get-access-token |
1 hour |
Credentials are cached with TTL awareness - Mino automatically refreshes expired tokens.
State Storage
~/.config/mino/config.toml # User configuration
# State directory (platform-specific):
# Linux: ~/.local/state/mino/
# macOS: ~/Library/Application Support/mino/
<state_dir>/mino/
+-- sessions/*.json # Session state
+-- credentials/*.json # Cached credentials (0o700 dir, 0o600 files)
+-- audit.log # Security audit log
Security Considerations
Mino provides defense-in-depth but is not a complete security solution:
- Container Hardening: All containers run with
--cap-drop ALL,--security-opt no-new-privileges, and--pids-limit 4096by default - Trust Boundary: The container can access anything mounted into it
- Network Access: Default
bridgemode isolates containers from host localhost. Use--network nonefor air-gapped sessions,--network-allowor--network-presetfor fine-grained egress control - Credential Scope: Temporary credentials still have the permissions of the source identity
- OrbStack Trust: You're trusting OrbStack's VM isolation
- Container Cleanup: All sessions (interactive and detached) remove containers after exit to prevent credential persistence via
podman inspect
For maximum security:
- Use dedicated cloud roles with minimal permissions
- Use named sessions to track activity
- Use
--network noneor--network-allowfor network-restricted sessions - Use
--network-preset registriesto limit egress to package registries only
Audit Log
Mino writes security events to <state_dir>/mino/audit.log in JSON Lines format. Enabled by default; disable with general.audit_log = false in config.
Each line is a JSON object:
Events
| Event | When | Data fields |
|---|---|---|
session.created |
Session state initialized | name, project_dir, image, command |
credentials.injected |
Cloud credentials passed to container | session_name, providers |
session.started |
Container running | name, container_id |
session.stopped |
Container exited | name, exit_code |
session.failed |
Container failed to start | name, error |
Audit logging uses silent failure mode — IO errors are logged via tracing::warn but never block or crash the primary workflow.
Development
# Build debug
# Build release
# Run tests
# Run with debug logging
RUST_LOG=mino=debug
# Format code
# Lint
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.