A secure, version-locked Podman DevContainer for AI agents (Cursor, Claude, OpenCode). Secrets are pulled from Vaultwarden via Touch ID and injected into the container's environment at runtime.
- Podman & DevContainers CLI:
brew install podman npm install -g @devcontainers/cli
- Bitwarden CLI & jq:
brew install bitwarden-cli jq
- Biometric Bridge (
bwbio):brew install jeanregisser/tap/bitwarden-cli-bio
Your Bitwarden/Vaultwarden Desktop app must be running, unlocked, and have "Allow browser integration" checked in settings.
Create the following entries in your vault:
| Vault Item Name | Type | Content |
|---|---|---|
| Anthropic API | Login | Password field: <API_KEY> |
| Cursor API | Login | Password field: <API_KEY> |
| AI GitHub PAT | Login | Password field: <GITHUB_PAT> |
| Custom Field | Text | Name: Git Name, Value: <Your Name> |
| Custom Field | Text | Name: Git Email, Value: <Your Verified GitHub Email> |
| AI SSH Key | Secure Note | The full id_ed25519 private block |
| AI GPG Key | Secure Note | The full exported .asc private block |
- Copy this entire directory to your preferred config location (default in script is
~/.config/devcontainers/fedora-sandbox/). - Append the
aiboxfunction from.zshrcinto your~/.zshrc. - Run
source ~/.zshrc.
To launch:
cd /path/to/any/project
aiboxTouch ID authenticates you, the container builds (or reuses cache), secrets are injected, Git signing is configured, and a terminal attaches.
Everything in this setup is pinned to enable reproducible builds and security auditing:
| Component | How It's Pinned | How to Update |
|---|---|---|
Base image (fedora:43) |
Version tag + SHA256 digest in Containerfile |
Dependabot opens PRs automatically |
| DNF packages | Exact name-version-release strings |
CI workflow opens PRs automatically (weekly) |
| Oh My Zsh | Git commit SHA | CI workflow opens PRs automatically (weekly) |
| prek | Release version tag | CI workflow opens PRs automatically (weekly) |
| VS Code extensions | Pinned publisher.name@version |
CI workflow opens PRs automatically (weekly) |
| GitHub SSH host keys | Embedded in container-init.sh |
CI workflow verifies monthly, opens PR on rotation |
| AI CLIs (Claude, Cursor, OpenCode) | Installed via curl|bash (see note below) |
Rebuild to pick up new versions |
Note on AI CLI installers: Claude, Cursor, and OpenCode are installed via vendor curl|bash scripts which cannot be version-pinned. The install script logs installed versions during build. Review the build log to audit what was installed.
This repo uses a combination of Dependabot and GitHub Actions workflows to keep every pinned dependency current. All updates arrive as PRs for human review — nothing auto-merges.
- Fedora base image — new digest PRs (docker ecosystem)
- DevContainer features — version updates (devcontainers ecosystem)
- Pre-commit hooks — revision updates (pre-commit ecosystem)
| Workflow | Schedule | What It Updates |
|---|---|---|
update-dnf-versions.yml |
Weekly (Mon) | DNF package version pins in Containerfile |
update-ohmyzsh.yml |
Weekly (Mon) | OHMYZSH_COMMIT SHA in Containerfile |
update-prek.yml |
Weekly (Mon) | PREK_VERSION tag in Containerfile |
update-extensions.yml |
Weekly (Mon) | Extension versions in devcontainer.json |
verify-github-ssh-keys.yml |
Monthly (1st) | SSH host keys in container-init.sh |
All update workflows can also be triggered manually via workflow_dispatch.
Every push and PR runs:
- Pre-commit hooks — trailing whitespace, EOF fixer, large file check, gitleaks
- ShellCheck — static analysis of all shell scripts
- Container build — validates the Containerfile builds successfully (catches broken version pins)
On every push to main and on PRs, the CI:
- Builds the container image
- Generates a CycloneDX SBOM using Syft (uploaded as a build artifact)
- Scans the SBOM for known vulnerabilities using Grype (results uploaded to GitHub Security tab)
For DNF packages, the update script can still be run manually inside a Fedora container:
bash scripts/update-dnf-versions.shThen paste the output into the Containerfile.
The runArgs in devcontainer.json intentionally weaken container isolation to support Podman-in-Podman:
--security-opt=seccomp=unconfined-- Allows all syscalls (needed for nested containers)--security-opt=systempaths=unconfined-- Exposes/procand/syspaths--userns=host-- Shares the host user namespace--cgroupns=host-- Shares the host cgroup namespace (required for nested Podman cgroup delegation)
The inner Podman is configured via /etc/containers/containers.conf in the image to use cgroupfs (not systemd) as its cgroup manager, and k8s-file as its log driver so podman logs works without journald.
This means the container boundary is not a strong security boundary. The sandbox relies on Podman's rootless mode and the ephemeral nature of the container for isolation.
- Secrets are fetched from Vaultwarden via Touch ID and passed as environment variables
- The
postStartCommandwrites the SSH key to the container filesystem (inside a volume) underumask 077(no world-readable window) - GPG keys are imported into the container's GPG keyring (inside a volume)
- After SSH/GPG setup, the base64-encoded secret env vars (
AI_SSH_KEY_B64,AI_GPG_KEY_B64) are unset so they don't linger for child processes - On the host, shell variables are explicitly
unsetwhen the launcher function exits (both success and failure paths); however, this does not guarantee the memory pages are zeroed by the OS - All
curl|bashinstallers enforce--proto '=https' --tlsv1.2to prevent TLS downgrade attacks
GitHub's SSH host keys are embedded directly in scripts/container-init.sh rather than using ssh-keyscan at runtime. This prevents MITM attacks during first connection. The verify-github-ssh-keys CI workflow checks monthly for key rotations and opens a PR if they change.