Isolated development environment using Linux kernel overlayfs.
boq allows:
- You to use
yoloor--dangerously-skip-permissionsmode without worrying about AI screwing up your Linux system. - You to instantly create an isolated environment that runs exactly like your familiar system.
- All your familiar commands and AI tools ready to use - no need to re-install, login, authenticate, or configure.
- Zero learning curve - same shell, same muscle memory, same dotfiles. It just feels like home.
- Full access to your entire
$HOME- all your projects, references, and personal scripts. Not trapped in a single project directory. - Your existing compilers, interpreters, and build cache ready to use - no re-downloading, no re-building.
- You to run multiple isolated environments simultaneously (
dev,experiment,feature-x) - each with independent file changes, but sharing the same build toolchain. - You to let multiple AI agents work in parallel on the same project, each in its own isolated environment.
- You to experiment fearlessly - don't like the result?
boq destroyand start fresh. No leftover config files, broken dependencies, or system pollution. - No Dockerfile to write, no image to build, instant start.
Note: Files unchanged by boq are read from the host in real-time. If you
git pullon the host, unmodified files will update inside the boq. Best practice: keep host files untouched while AI is working, and sync periodically.
- Configurable overlay directories (default:
$HOME,/usr,/opt,/home/linuxbrew) - Passthrough paths that bypass overlay and share with host
- Full file locking support (unlike fuse-overlayfs)
- TOML-based configuration with 3-tier override system
- In default bridge mode, each boq gets a static IP and a host alias
boq-<name>. - From host, you can access services inside boq directly, for example:
curl http://boq-dev:8080. - Legacy rootless running boqs do not provide this host-access alias; restart them with
boq stop <name>thenboq enter <name>to upgrade. - For docker-backed boqs (
create --runtime docker), boq auto-manages a user-defined networkboq-docker-net. Its subnet is auto-detected once and persisted in~/.boq/.docker-subnet. - With docker runtime,
container.networkonly supportshost/none(or unset). Any other configured network name is rejected.
Requires Python 3.11+ and one container runtime: podman or docker.
# Install one runtime (choose one)
sudo apt install podman
# or
sudo apt install docker.io
# Install boq (choose one)
pipx install boq # Recommended: isolated global install
uv tool install boq # Alternative: using uv
pip install boq # Or: install to current environment
# For development
git clone https://github.com/zhengbuqian/boq.git
cd boq
uv pip install -e . # Editable installOptionally, add the following to your ~/.bashrc or ~/.zshrc to get auto completion and auto yolo mode:
if [ -z "$BOQ_NAME" ]; then
# add auto completion only in host.
eval "$(boq completion -s zsh 2>/dev/null)"
else
echo "In boq env, using yolo mode by default for AI cli tools."
alias claude="claude --dangerously-skip-permissions"
alias codex="codex --dangerously-bypass-approvals-and-sandbox"
alias gemini="gemini --yolo"
# if you use zsh and see error like `zsh: can't rename ~/.zsh_history.new to $HISTFILE`,
# you can suppress this error by unsetting `HIST_SAVE_BY_COPY` option.
unsetopt HIST_SAVE_BY_COPY
fi
# I personally also use this to know where am I, in host or in some boq.
PROMPT="[%m] ${PROMPT}"Note: Requires sudo for mounting kernel overlayfs.
# Create a boq and enter it (exit shell to detach, container keeps running)
boq create dev
# Re-enter existing boq
boq enter dev
# Open VS Code attached to boq
boq code dev
# Create and open in VS Code directly
boq create dev --code
# See what changed
boq diff dev
# Run a command in boq
boq run dev "make test"
# Stop boq
boq stop dev
# Remove boq
boq destroy dev| Command | Description |
|---|---|
create <name> |
Create a new boq and enter it (use --no-enter to skip, --code to open VS Code, --runtime to pick backend, and --docker-sudo/--no-docker-sudo for docker mode) |
enter [name] |
Attach shell to boq (starts if not running; use --migrate-to-docker to migrate a stopped boq to docker) |
code [name] |
Open VS Code attached to boq (starts if not running; use --workdir to set working directory) |
run <name> <cmd> |
Run a command in boq (must be running; may be interrupted by stop/destroy) |
stop [name] |
Stop a boq immediately (may interrupt active sessions) |
destroy <name> |
Destroy a boq immediately (stops if running; may interrupt active sessions) |
diff [name] [path] |
Show changes made in boq |
status [name] |
Show boq status |
list [--size] |
List all boq instances (--size also shows disk usage) |
version |
Show current boq version |
completion -s <shell> |
Output shell completion script |
Default name is default for commands that accept [name].
boq diff dev # Show all content changes
boq diff dev ~/project # Filter by path (respects .gitignore)
boq diff dev --no-gitignore # Include gitignored files
boq diff dev --include-metadata # Include metadata-only changes# Bash: add to ~/.bashrc
eval "$(boq completion -s bash)"
# Zsh: add to ~/.zshrc
eval "$(boq completion -s zsh 2>/dev/null)"TOML-based configuration with 3-tier override system:
- defaults.toml (shipped with package) - base defaults
- ~/.boq/config.toml (user global) - override defaults
- ~/.boq/
<name>/config.toml (per-boq) - override for specific boq
Higher priority overrides lower. Lists append by default (use <key>_replace to fully replace).
[container]
# Change default shell
shell = "/bin/zsh"
# Change base image
image = "ubuntu:24.04"
[runtime]
# auto: prefer docker when available, else podman
default = "auto"
[docker]
# default for create when runtime is docker
use_sudo = false
[container.env]
# Add custom environment variables
MY_VAR = "value"
[overlays]
# Add additional overlay directory
"/data" = "data"
[passthrough]
# Add paths that bypass overlay (appends to default list)
paths = [
"$HOME/.my-tool",
]
# Or replace the entire list
paths_replace = [
"$HOME/.zsh_history",
"$HOME/.claude",
]
[mounts]
# Custom mounts: mount a file/directory to a different location in the container
# Each entry specifies: src (host path), dest (container path), mode ("ro" or "rw")
custom = [
{ src = "/path/on/host", dest = "/path/in/container", mode = "ro" },
][container]
image = "ubuntu:22.04"
shell = "/bin/bash"
capabilities = ["SYS_PTRACE"]
[runtime]
default = "auto"
[docker]
use_sudo = false
[overlays]
"$HOME" = "home"
"/usr" = "usr"
"/opt" = "opt"
"/home/linuxbrew" = "linuxbrew"
[passthrough]
# sharing shell history and AI cli tools config/history bewteen boq and host by default
paths = [
"$HOME/.zsh_history",
"$HOME/.bash_history",
"$HOME/.claude",
"$HOME/.gemini",
"$HOME/.codex",
"$HOME/.factory",
]
[mounts]
readonly = ["/bin", "/lib", "/lib64", "/lib32", "/sbin"]
direct = []
custom = []Environment variable expansion ($HOME, $USER, etc.) is supported in all string values.
createsets up overlays, starts container, and enters shell (use--no-enterto skip,--codeto open VS Code instead)enterattaches a shell; exiting detaches but container stays runningcodeopens VS Code attached to the container via Dev Containers extension (starts container if needed)createpicks runtime automatically by default (prefer docker when available, else podman)create --runtime docker|podmanexplicitly selects runtimecreate --docker-sudo/--no-docker-sudocontrols docker command prefix for that boqenter --migrate-to-dockercan migrate a stopped podman boq to docker backendrunexecutes a single command (container must be running)stopimmediately stops container and unmounts overlays (active sessions may be interrupted)destroyimmediately stops/removes container and deletes boq files (active sessions may be interrupted)- Container manages its own
/proc,/sys,/dev,/tmp
Multiple directories are overlayed (copy-on-write) using kernel overlayfs. Changes are stored in ~/.boq/<name>/<overlay>/upper/.
/bin,/lib,/lib64,/lib32,/sbin- essential system directories, read-only from host
Symptom: If you run git pull on the host while boq is running, new files appear inside the boq.
Cause: Overlayfs lowerdir is live, not a snapshot.
Workaround: Do NOT modify files on the host while boq is running.
- To update code: run
git pullinside the boq, OR - Stop the boq first, update on host, then re-enter
Symptom: You have a symlink like ~/.ccache -> /mnt/nvme1n1p1/.ccache, but inside boq the symlink is broken:
$ ls -la ~/.ccache
lrwxrwxrwx 1 user user 22 Feb 21 2024 .ccache -> /mnt/nvme1n1p1/.ccache
$ ls ~/.ccache/
ls: cannot access '/home/user/.ccache/': No such file or directoryCause: By default, /mnt (or other mount points for external drives) is not mounted inside boq. The symlink exists in the $HOME overlay, but its target doesn't exist.
Solutions: There are three approaches, each with different trade-offs:
Mount the entire parent directory directly:
[mounts]
direct = ["/mnt"]- Symlink resolves correctly (
~/.ccache→/mnt/nvme1n1p1/.ccache) - Read-write: changes in boq are immediately visible on host
- Use case: You want to share build cache between host and boq (e.g., ccache, pip cache)
Add the symlink path to passthrough:
[passthrough]
paths = ["$HOME/.ccache"]~/.ccachebecomes accessible (podman resolves the symlink and mounts the target directly)- Note: Inside boq,
~/.ccacheappears as a directory, not a symlink - Read-write: changes in boq are immediately visible on host
- Use case: You only need one specific path shared, not the entire
/mnt
Overlay each mounted drive separately:
[overlays]
"/mnt/nvme1n1p1" = "nvme1n1p1"
"/mnt/nvme2n1p1" = "nvme2n1p1"- Symlink resolves correctly (
~/.ccache→/mnt/nvme1n1p1/.ccache) - Copy-on-write: changes in boq are isolated, host doesn't see them
- Changes stored in
~/.boq/<name>/nvme1n1p1/upper/ - Use case: You want to experiment with cached data without affecting host cache
Important: You cannot overlay
/mntdirectly if it contains nested mount points (e.g.,/mnt/nvme1n1p1,/mnt/nvme2n1p1). Overlayfs cannot see through mount points - you'll only see empty directories. You must overlay each mount point individually.
| Solution | Access via symlink | Host sees changes | Use case |
|---|---|---|---|
direct = ["/mnt"] |
✅ | ✅ Yes | Share cache with host |
passthrough = ["$HOME/.ccache"] |
✅ (as directory) | ✅ Yes | Share specific path only |
overlays per mount point |
✅ | ❌ No (isolated) | Experiment without affecting host |
Error: "Temporary failure in name resolution"
Cause: systemd-resolved uses a stub resolver at an address that doesn't work inside the container.
Solution: boq auto-detects your DNS configuration:
- If systemd-resolved is in use, it mounts
/run/systemd/resolve/resolv.conf(with actual upstream DNS servers) - Otherwise, it falls back to
/etc/resolv.conf
If auto-detection doesn't work for your system, use a custom mount to override:
[mounts]
custom = [
{ src = "/path/to/your/resolv.conf", dest = "/etc/resolv.conf", mode = "ro" },
]Since boq isolates files via overlayfs, editors on the host can't directly see changes made inside boq. Here are three approaches:
For VS Code, use the built-in boq code command which handles everything automatically:
boq code dev # Open VS Code attached to running boq
boq create dev --code # Create boq and open VS Code directly
boq code dev -w ~/project # Open a specific directoryThis requires VS Code CLI (code) in PATH and the Dev Containers extension (ms-vscode-remote.remote-containers). boq code will validate both and show installation instructions if missing.
Alternatively, attach manually via the Dev Containers extension:
- Install "Dev Containers" extension
Ctrl+Shift+P→ "Dev Containers: Attach to Running Container..."- Select
boq-<name>
You get full editor experience inside the container with all your build cache and intermediate results. Changes stay isolated.
Note (rootful vs rootless):
- If your boq container is running with rootful podman, Dev Containers may not see it via plain
podman.- Create a wrapper command and point Dev Containers to it:
mkdir -p ~/.local/bin cat > ~/.local/bin/sudopodman <<'EOF' #!/usr/bin/env bash exec sudo -n /usr/bin/podman "$@" EOF chmod +x ~/.local/bin/sudopodmanThen set Dev > Containers: Docker Path to
sudopodman.If you're still using rootless containers, keep using
podman.
Create a separate working copy that's shared between host and boq:
# On host
git worktree add ../my-project-boq feature-branch# ~/.boq/config.toml
[passthrough]
paths = ["$HOME/projects/my-project-boq"]Edit on host, build inside boq. Multiple boqs can work on different worktrees without interference. Downside: no shared build cache—each worktree builds from scratch.
[passthrough]
paths = ["$HOME/projects/my-project"]Simplest setup—edit on host, run in boq. But only one boq can safely work on this project at a time (changes are shared).
| Approach | Build cache | Multiple boqs | Edit from host |
|---|---|---|---|
| Dev Containers | ✅ Yes | ✅ Yes | ❌ No (edit in container) |
| Git worktree + passthrough | ❌ No | ✅ Yes | ✅ Yes |
| Project passthrough | ✅ Yes | ❌ No (conflicts) | ✅ Yes |
Why kernel overlayfs instead of fuse-overlayfs?
- fuse-overlayfs mounts are only accessible by the user who created them (permission issues with podman)
- fuse-overlayfs doesn't fully support POSIX file locking
- Kernel overlayfs requires sudo but provides full compatibility