The Coding Agent Sandbox
providing system isolation and automated change tracking
for OpenCode and (soon) others.
cont[AI]n provides a secure, containerized environment for running an AI coding assistant that can read and write files in your project directories while maintaining strict isolation from the rest of your system.
- Overview
- Architecture
- Prerequisites
- Installation
- Usage
- Configuration Reference
- How It Works
- Security Considerations
- Troubleshooting
- Uninstallation
- Contributing
- License
cont[AI]n creates a sandboxed environment where an AI coding agent (OpenCode) can:
- Read and modify files in designated project directories
- Run in isolation from your host system
- Persist tool installations across container restarts
- Track file changes with automatic permission management
- Commit container state periodically for durability
- Rootful Podman container with UID/GID mapping to host users
- Systemd integration via Podman Quadlet for service management
- File watcher service that maintains correct permissions on new files
- Periodic container commits to preserve installed tools and state
- Interactive TUI that attaches to the running headless server
- JSON-based configuration for easy customization
┌─────────────────────────────────────────────────────────────────────────┐
│ HOST SYSTEM │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │
│ │ Primary User │ │ File Watcher │ │ Commit Timer │ │
│ │ (e.g., alice) │ │ (systemd) │ │ (systemd) │ │
│ │ │ │ │ │ │ │
│ │ - Owns files │ │ - Monitors │ │ - Runs daily │ │
│ │ - Agent shares │ │ project dirs │ │ - Commits │ │
│ │ user's group │ │ - Fixes perms │ │ container │ │
│ └──────────────────┘ └──────────────────┘ └─────────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ /home/alice/Projects (example) │ │
│ │ │ │
│ │ - Group: primary user's group (agent shares it) │ │
│ │ - Directories: g+x (traverse) │ │
│ │ - Files: g+r (read), add g+w for write │ │
│ │ - Sensitive files (.env, secrets/): mode 700 (agent blocked) │ │
│ │ - .git/: read-only for group │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ bind mount │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ PODMAN CONTAINER │ │
│ │ (contain) │ │
│ ├──────────────────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ OpenCode Server │ │ │
│ │ │ │ │ │
│ │ │ - Runs as 'agent' user (UID mapped to host) │ │ │
│ │ │ - Listens on 127.0.0.1:3000 │ │ │
│ │ │ - Headless mode (--hostname, --port) │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Pre-defined Mounts │ │
│ │ - ~/.config/opencode → /home/agent/.config/opencode (ro) │ │
│ │ - ~/.local/share/opencode → /home/agent/.local/share/... (rw) │ │
│ │ - ~/.local/state/opencode → /home/agent/.local/state/... (rw) │ │
│ │ - ~/.config/contain/config.json → /etc/contain/ (ro) │ │
│ │ │ │
│ │ Mounts (paths are examples, configured via config.json): │ │
│ │ - /home/alice/Projects → /workspace/Projects (rw) │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Note: In the diagram above, ~ refers to the primary user's home directory
(e.g., /home/alice for user alice). Project directories are mounted under
/workspace inside the container, with the common parent directory stripped
(e.g., /home/alice/Projects becomes /workspace/Projects).
Before installing cont[AI]n, ensure your system meets the following requirements:
| Dependency | Version | Purpose |
|---|---|---|
| Linux | Any modern distro | Host operating system |
| systemd | 250+ | Service management |
| Podman | 4.0+ | Container runtime (rootful mode) |
| jq | 1.6+ | JSON parsing in scripts |
| Dependency | Purpose |
|---|---|
| inotify-tools | File watcher service (host-side) |
| git | Version control in container |
You'll need API credentials for at least one LLM provider. See the OpenCode documentation for supported providers and authentication setup.
Fedora/RHEL/CentOS:
sudo dnf install podman jq inotify-toolsUbuntu/Debian:
sudo apt update
sudo apt install podman jq inotify-toolsArch Linux:
sudo pacman -S podman jq inotify-toolsNixOS:
See nix/README.md for the declarative NixOS flake module.
# Clone the repository
git clone https://github.com/j-i-l/cont-AI-nerd.git
cd cont-AI-nerd
# Configure (interactive)
sudo ./scripts/configure.sh
# Run setup
sudo ./scripts/setup.sh
# Start the TUI (with auth capability)
sudo contain-tuigit clone https://github.com/j-i-l/cont-AI-nerd.git
cd cont-AI-nerdRun the interactive configuration script to create your settings file:
sudo ./scripts/configure.shYou'll be prompted for the following settings:
=================================================================
contain — Configuration
=================================================================
This script will create the configuration file for contain.
Press Enter to accept the default value shown in brackets.
Primary user [alice]:
Home directory for alice [/home/alice]:
Project directories (comma-separated) [/home/alice/Projects]:
Container agent username [agent]:
Server listen address [127.0.0.1]:
Server listen port [3000]:
Installation directory [/opt/contain]:
The configuration is saved to ~/.config/contain/config.json.
If you prefer to create the configuration file manually:
mkdir -p ~/.config/contain
cat > ~/.config/contain/config.json << 'EOF'
{
"primary_user": "your-username",
"primary_home": "/home/your-username",
"project_paths": [
"/home/your-username/Projects",
"/home/your-username/work"
],
"agent_user": "agent",
"host": "127.0.0.1",
"port": 3000,
"install_dir": "/opt/contain"
}
EOF
chmod 640 ~/.config/contain/config.jsonRun the setup script to build the container and configure services:
sudo ./scripts/setup.shThe setup script will:
- Provision identity — Create the
agentuser (sharing the primary user's group) - Configure permissions — Set up project directory traversal
- Generate policies — Create OpenCode permission policies
- Create directories — Ensure OpenCode config/data directories exist
- Build container — Build the contain container image
- Install scripts — Copy helper scripts to
/opt/contain - Install systemd units — Set up Quadlet and service files
- Activate services — Start the container and auxiliary services
Upon completion, you'll see:
=================================================================
contain setup complete.
Container : podman ps | grep contain
TUI : sudo contain-tui
Watcher : systemctl status contain-watcher
Commits : systemctl list-timers contain-commit
Logs : journalctl -u contain -f
=================================================================
sudo contain-tuiThis attaches an interactive TUI to the headless OpenCode server running inside the container. You can use /connect to authenticate with your LLM provider — credentials are saved automatically and persist across sessions (no restart needed).
On first use, run /connect in the TUI to authenticate with your preferred LLM provider (e.g., GitHub Copilot). The server writes credentials to ~/.local/share/opencode/auth.json and reloads providers automatically.
# Start with a specific session
sudo contain-tui --session <session-id>
# Start in a specific directory (container path)
sudo contain-tui --dir /workspace/Projects/myproject# View running container
podman ps | grep contain
# Detailed container info
podman inspect contain# Container service (via systemd generator)
systemctl status contain
# File watcher service
systemctl status contain-watcher
# Commit timer
systemctl list-timers contain-commit# Container logs (follow mode)
journalctl -u contain -f
# File watcher logs
journalctl -u contain-watcher -f
# Commit service logs
journalctl -u contain-commit
# All contain related logs
journalctl -u 'contain*' --since "1 hour ago"The configuration file is located at ~/.config/contain/config.json.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
primary_user |
string | Yes | — | The primary user who owns project files |
primary_home |
string | Yes | — | Home directory of the primary user |
project_paths |
array | Yes | — | List of directories the agent can access |
agent_user |
string | No | "agent" |
Username for the container agent |
host |
string | No | "127.0.0.1" |
Address the server listens on |
port |
number | No | 3000 |
Port the server listens on |
install_dir |
string | No | "/opt/contain" |
Where helper scripts are installed |
{
"primary_user": "alice",
"primary_home": "/home/alice",
"project_paths": [
"/home/alice/Projects",
"/home/alice/work",
"/home/alice/oss"
],
"agent_user": "agent",
"host": "127.0.0.1",
"port": 3000,
"install_dir": "/opt/contain"
}To update your configuration:
# Interactive reconfiguration
sudo ./scripts/configure.sh
# Then re-run setup
sudo ./scripts/setup.sh
# Select "recreate" when prompted, or press Enter to use existing configcont[AI]n uses rootful Podman to create a container that:
- Maps UIDs 1:1 — The
agentuser inside the container has the same UID as the hostagentuser - Uses host networking — Simplifies access; server binds to localhost only
- Mounts specific directories — Only project directories are accessible read-write
- Mounts config read-only — OpenCode configuration is read-only
- Mounts data read-write — OpenCode data (database, credentials, model state) is read-write
- Limits resources — Container is restricted to 2GB RAM and 100 processes
The permissions model uses standard Unix group permissions. The agent inside the container shares the primary user's group GID directly — no dedicated group is needed. You opt in by adding directories to projectPaths in the configuration:
Primary User (e.g., alice) Agent User (agent)
│ │
│ primary group │ mapped to same GID
▼ ▼
┌─────────────────────────────────────────┐
│ Primary user's group │
│ │
│ Permission Levels (you choose): │
│ │
│ ┌─────────────────────────────────┐ │
│ │ g+rw → agent can READ + WRITE │ │
│ │ g+r → agent can READ only │ │
│ │ g= → agent BLOCKED │ │
│ └─────────────────────────────────┘ │
│ │
│ Directory requirements: │
│ - g+x for traverse access │
│ - g+rx for read/traverse access │
│ - g+rwx for read/write access │
│ │
└─────────────────────────────────────────┘
Setting agent access:
| Goal | Command |
|---|---|
| Read + Write | chmod g+rw file |
| Read only | chmod g+r,g-w file |
| Blocked | chmod 600 file |
For directories, add the execute bit:
# Make directory traversable
chmod g+x ~/Projects/myproject
# Recursively grant read+write access
chmod -R g+rw ~/Projects/myproject
find ~/Projects/myproject -type d -exec chmod g+x {} \;Key aspects:
- Opt-in model — By adding a directory to
projectPaths, you opt in to the agent accessing files in that directory - Standard group permissions — The agent shares the primary user's group; no special group is created
- Granular access — Set permissions per-file based on sensitivity
.git/read-only — The prepare script sets.git/to read-only automatically- File watcher — Fixes ownership on files created by the agent
The prepare-permissions.sh script makes project directories traversable for the agent. It does not change individual file permissions — you control what the agent can read/write.
# Preview changes (dry-run)
sudo ./scripts/prepare-permissions.sh --dry-run ~/Projects
# Make directories traversable
sudo ./scripts/prepare-permissions.sh ~/Projects
# Use paths from config.json
sudo ./scripts/prepare-permissions.sh --from-configWhat the script does:
| Target | Action | Result |
|---|---|---|
.git/ directories |
chmod -R g=rX,g-w |
Agent can read history, cannot modify |
| Other directories | chmod g+x |
Agent can traverse |
| Sensitive files | unchanged by default | Your existing permissions preserved |
| Regular files | unchanged | Set permissions yourself |
Handling sensitive files:
The script detects sensitive files (.env, secrets/, *.key, etc.) but does not lock them by default. You have three options:
# Option 1: Lock all sensitive files automatically
sudo ./scripts/prepare-permissions.sh --lock-sensitive ~/Projects
# Option 2: Interactive prompt (default when running in terminal)
sudo ./scripts/prepare-permissions.sh ~/Projects
# → Script will ask: "Lock these from the agent? [1] Yes [2] No"
# Option 3: Skip sensitive file handling entirely
sudo ./scripts/prepare-permissions.sh --no-lock-sensitive ~/ProjectsWhen locked, sensitive files get mode 600 (files) or 700 (directories), making them inaccessible to the agent.
Sensitive patterns detected:
- Environment:
.env,.env.*,*.env - Secrets:
secrets/,.secrets/,vault/,credentials/ - Keys:
*.pem,*.key,id_rsa,id_ed25519, etc. - Auth:
.npmrc,.pypirc,.netrc,*auth*.json - Databases:
*.sqlite,*.db
cont[AI]n installs three systemd components:
| Component | Type | Purpose |
|---|---|---|
contain.service |
Quadlet (generated) | Runs the container |
contain-watcher.service |
Service | Monitors files, fixes permissions |
contain-commit.timer |
Timer | Triggers periodic container commits |
Quadlet Integration:
The container is managed via Podman Quadlet, which generates a systemd service from the .container file in /etc/containers/systemd/. This provides:
- Automatic container start on boot
- Proper dependency ordering
- Integration with systemd tooling
The commit timer runs hourly to persist container state:
# View timer schedule
systemctl list-timers contain-commit
# Manual commit
sudo systemctl start contain-commitThis preserves:
- Installed tools in
/opt/tools - Package manager caches
- Any container filesystem changes
- Project directories (read-write) — Only paths listed in
project_paths - OpenCode config (read-only) — Your OpenCode settings and themes
- OpenCode data (read-write) — Session data, credentials, and model preferences
- Host system files — No access outside mounted paths
- Other users' files — Only the primary user's directories
- Network services — Binds to localhost only (127.0.0.1)
- System configuration — No access to
/etc,/var, etc. - Privileged operations — Runs as unprivileged
agentuser
An OpenCode policy file is automatically generated at ~/.config/contain/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"external_directory": {
"/home/alice/Projects/**": "allow"
}
}
}This explicitly allows OpenCode to access only the configured project paths.
The server binds to 127.0.0.1 by default, meaning:
- Only local connections are accepted
- The server is not accessible from other machines
- No firewall configuration is needed
To expose the server to other machines (not recommended), change host to 0.0.0.0 in the config.
OpenCode credentials are stored in ~/.local/share/opencode/auth.json on the host. The ~/.local/share/opencode/ and ~/.local/state/opencode/ directories are mounted read-write into the container, so /connect can save credentials directly. After authentication, the server reloads providers automatically — no container restart is needed.
Check the service status:
systemctl status contain
journalctl -u contain -n 50Common causes:
- Port already in use: Change
portin config and re-run setup - Image not built: Run
sudo ./scripts/setup.shagain - Missing config file: Run
sudo ./scripts/configure.sh
Check file permissions:
ls -la ~/Projects/Use the permission preparation script:
# Preview what will change
sudo ./scripts/prepare-permissions.sh --dry-run ~/Projects
# Apply secure permissions
sudo ./scripts/prepare-permissions.sh ~/ProjectsThis script sets appropriate permissions while protecting sensitive files (.env, secrets, keys) and keeping .git/ read-only.
Ensure the container is running:
podman ps | grep containCheck the server is listening:
podman exec contain ss -tlnp | grep 3000Try restarting the container:
sudo systemctl restart containCheck the service:
systemctl status contain-watcher
journalctl -u contain-watcher -fEnsure inotify-tools is installed:
which inotifywait
# If not found:
sudo dnf install inotify-tools # Fedora
sudo apt install inotify-tools # UbuntuVerify credentials are mounted:
sudo podman exec contain ls -la /home/agent/.local/share/opencode/Re-authenticate using the TUI:
sudo contain-tui
# Then run: /connectThe container is limited to 2GB RAM. To increase:
- Edit
systemd/contain.container.in - Change
--memory 2gto a higher value - Re-run
sudo ./scripts/setup.sh
To completely remove cont[AI]n:
# Stop and disable services
sudo systemctl stop contain-watcher
sudo systemctl stop contain-commit.timer
sudo systemctl stop contain
sudo systemctl disable contain-watcher
sudo systemctl disable contain-commit.timer
# Remove systemd units
sudo rm /etc/systemd/system/contain-watcher.service
sudo rm /etc/systemd/system/contain-commit.service
sudo rm /etc/systemd/system/contain-commit.timer
sudo rm /etc/containers/systemd/contain.container
sudo systemctl daemon-reload
# Remove the container and image
sudo podman rm -f contain
sudo podman rmi localhost/contain:latest
# Remove helper scripts
sudo rm -rf /opt/contain
# Remove configuration (optional)
rm -rf ~/.config/contain
# Remove the agent user (optional)
sudo userdel agentContributions are welcome! Please follow these guidelines:
- Fork the repository
- Clone your fork:
git clone https://github.com/j-i-l/cont-AI-nerd.git - Create a branch:
git checkout -b feature/your-feature
- Shell scripts: Use
shellcheckfor linting - Indentation: 2 spaces for shell scripts
- Comments: Use descriptive comments for complex logic
- Error handling: Always use
set -euo pipefailin scripts
Follow conventional commit format:
type(scope): description
[optional body]
[optional footer]
Types: feat, fix, docs, style, refactor, test, chore
Examples:
feat(container): add support for custom resource limits
fix(watcher): handle spaces in directory names
docs(readme): add troubleshooting section for SELinux
- Ensure your code follows the style guidelines
- Update documentation if needed
- Test your changes on a clean system if possible
- Create a pull request with a clear description
- Reference any related issues
Before submitting:
# Lint shell scripts
shellcheck scripts/*.sh lib/*.sh container/*.sh
# Test the full setup process
sudo ./scripts/configure.sh
sudo ./scripts/setup.sh
podman exec -it contain opencode-tuiWhen reporting bugs, please include:
- Operating system and version
- Podman version (
podman --version) - Relevant log output (
journalctl -u contain) - Steps to reproduce
This project is licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0).
You are free to:
- Share — copy and redistribute the material
- Adapt — remix, transform, and build upon the material
Under the following terms:
- Attribution — You must give appropriate credit
- NonCommercial — You may not use the material for commercial purposes
For commercial licensing inquiries, please contact the maintainers.
See LICENSE for the full license text.