Article Information

Category: Development

Published: 25 March, 2026

Author: Chris de Gruijter

Reading Time: 12 min

Tags

linuxsecuritycontainerspodmandistroboxvibe-codingai
Secure Linux development environment with containers and terminal

How I Built a Bulletproof Vibe-Coding Environment on Linux

Published: 25 March, 2026

Vibe-coding with AI is genuinely useful. Claude, Gemini, Copilot — these tools write real code, fast. But they also introduce a threat surface that most developers aren't thinking about. The AI can suggest a package name that doesn't exist and a malicious lookalike gets installed instead (slopsquatting). An IDE extension with broad file access can read your .env files and exfiltrate API keys. A compromised npm install script can do damage before you've even looked at the code.

I spent a few days building an environment that lets me vibe-code freely without any of those risks. This post documents exactly how it works, what it protects against, and how I use it day-to-day.

The Mental Model: House, Workshop, and Safe

Before getting into the technical detail, here's the mental model that underpins the whole setup:

  • Your host machine is the house. SSH keys and real secrets live here. Normal computer.
  • Your ~/Projects/ folder is the workshop — all code, zero secrets. Containers are allowed in here.
  • Your ~/Secrets/ folder is the safe — API keys, .env files, deploy credentials. Containers never see it.

This separation is the foundation. If a container is ever compromised, the blast radius is limited to code files. It cannot reach your SSH keys, API keys, or deployment credentials.

Three Environments

The setup has three environments, each with a specific purpose:

vibebox is a pure Podman container built from a custom Fedora image. It's truly isolated — only /Projects is mounted, plus per-project secrets mounted read-only. This is where all AI-assisted development happens. Claude Code, Gemini CLI, and AntiGravity IDE all run inside the container. The IDE window appears on my host screen via X11 forwarding, so it looks completely native, but everything is sandboxed.

devbox is a Distrobox container (also Fedora-based). Distrobox integrates more deeply with the host — it shares the host filesystem — which makes it feel like a normal terminal with better isolation than running on the host directly. I use this for trusted, non-AI terminal work: git operations, running build scripts, anything that doesn't involve AI writing or executing code.

testbox is a throwaway Distrobox container for vetting unknown packages before installing them anywhere else. It runs with firejail --net=none, so packages can't phone home during install.

The One Rule

Always vibe-code inside vibebox, not devbox.

— The rule I put at the top of my devbox.md

devbox shares the host filesystem via Distrobox. That means it can reach ~/Secrets/ by absolute path. It's a more convenient environment, which makes it tempting — but if you're doing AI-assisted work in devbox, the AI (or anything it installs) can read your secrets. vibebox cannot. The line has to stay firm.

Inside vibebox: What Makes It Secure

The vibebox image is built from a custom Containerfile. Several security settings are baked in at image build time rather than applied at runtime, which means they can't be accidentally overridden:

  • npm ignore-scripts=true — malicious install scripts never run. Even if the AI suggests a slopsquatted package and it gets installed, the install-time payload is blocked.
  • No host home mounted — SSH keys are completely invisible inside the container.
  • Secrets mounted read-only — only the secrets for the current project are mounted, at the same absolute path they live on the host. The app can read its .env, but nothing in the container can write to it.
  • SSH is opt-in — git push requires passing --ssh to the vibebox launch script. Without it, the SSH agent socket isn't forwarded.

The tools installed in the image are: git, Node 20, npm, pnpm 9, Claude Code, Gemini CLI, AntiGravity IDE, Google Chrome (for AntiGravity's browser integration), Python 3, and gcc/make.

Making Config Persist Across Container Restarts

Containers are ephemeral by default. Without explicit volume mounts, everything you configure inside vibebox disappears when the container stops. I mount the following host directories into the container so auth and settings survive restarts:

  • ~/.claude → Claude Code auth and settings
  • ~/.gemini → Gemini CLI auth
  • ~/.gitconfig → git user name and email
  • ~/.config/Antigravity → IDE settings, keybindings, themes, workspace history (mounted read-write — AntiGravity writes lock files on startup)
  • ~/.antigravity → installed extensions
  • ~/.codex → Codex (openai.chatgpt) auth, which stores credentials outside the IDE config directory
  • ~/.vibebox-keyrings → a separate GNOME Keyring store for container extension OAuth tokens, isolated from the host keyring

The keyring setup deserves a note: AntiGravity extensions that use VS Code's secrets API store credentials in GNOME Keyring. The container gets its own keyring directory (not the host one), so extension secrets are fully isolated — the MCP secrets stored there are encrypted via GNOME libsecret and the host keyrings are never mounted.

The Secrets Migration

When I started building this setup, secrets were scattered across project directories. Every project had a .env file sitting inside ~/Projects/Webfluentia/<project>/. That meant any container with access to /Projects could read them.

I physically moved every secret out of /Projects and into a new ~/Secrets/Webfluentia/ tree that mirrors the project structure exactly. The mapping is 1-to-1:

# Old location (inside /Projects — wrong)
~/Projects/Webfluentia/gevelpro-next-wf/.env

# New location (in /Secrets — correct)
~/Secrets/Webfluentia/gevelpro-next-wf/.env

# Symlink left in /Projects so the app still finds it
~/Projects/Webfluentia/gevelpro-next-wf/.env  →  symlink to /Secrets/...

Inside vibebox, the secret is mounted at its absolute host path (/home/cjvdeg/Secrets/Webfluentia/<project>), so the symlink in /projects/ resolves correctly. This matters because Podman can't bind-mount directly over a symlink — mounting the directory at its absolute path sidesteps that entirely.

After the migration, /Projects is completely secret-free. I could wipe the entire directory and lose no credentials.

How Aliases Work Across Environments

One friction point with multi-environment setups is that you rebuild your muscle memory for different contexts. I avoided this by mounting ~/.bashrc read-only into both containers and sourcing it at session start. All project aliases work identically on the host, inside devbox, and inside vibebox.

Inside vibebox, a small ~/.vibebox-rc init script sources the host bashrc and sets a [vibebox] PS1 prefix so I always know which environment I'm in. The ~/Projects path is symlinked to /projects inside the container so host bashrc paths resolve without modification.

A typical project alias looks like this inside devbox/vibebox:

# On host and inside every container — identical
alias gevelpro='cd $GEVELPRO && ag . && pdev'
# → cd to project, open AntiGravity, start dev server

Dev Server Port Forwarding

Ports 3000–3010 are always forwarded from the container to the host. No extra flags needed — just run pnpm dev inside vibebox and open http://localhost:3000 in the host browser as normal.

One technical note: vibebox uses --network=host rather than port mapping. This is because rootless Podman's port mapping (via the rootlessport process) produced conmon JSON errors when mapping port ranges. With --network=host, the container shares the host network namespace, so dev server ports are directly accessible. The tradeoff is that container processes can also reach services listening on host localhost — fine for my setup since I don't run sensitive local databases.

Daily Workflow

In practice the workflow is simple:

  1. Start vibebox for the project: ~/vibebox.sh Webfluentia/gevelpro-next-wf. This launches the container with the correct secrets mounted.
  2. Run the project alias inside: gevelpro. This cds to the project, opens AntiGravity IDE on my host screen, and starts the dev server.
  3. Vibe-code normally. Claude Code, Gemini, and all AI integrations run inside the sandboxed container. I work exactly as I would on the host.
  4. git push when needed: re-launch with --ssh flag: ~/vibebox.sh Webfluentia/gevelpro-next-wf --ssh.
  5. Unknown package? Run ~/safe-install.sh some-package first. This installs it in testbox with no network, no scripts — I can inspect before bringing it into a real project.

What It Protects Against

Here's the threat model in plain terms:

  • Malicious npm install scriptsignore-scripts=true is baked into the image. Even if an AI suggests a package with a malicious postinstall script, it never runs.
  • Slopsquatting (AI hallucinates a package name, real package with that name is malicious) — same protection. The install payload is blocked. For truly unknown packages, testbox adds a network-isolated inspection step.
  • SSH key theft — SSH keys are not accessible inside any container by default. The host home is never mounted.
  • API key / .env theft via AI code or extensions.env symlinks in /Projects/ point to real secrets in /Secrets/. Inside the container, secrets are mounted read-only. The AI can read the values it needs to run the app — but it cannot exfiltrate them via the filesystem because it has no path to /Secrets/ itself.
  • Production credential theft — deploy secrets are not in /Projects/ and are mounted read-only in vibebox only for the specific project being worked on.
  • System-wide compromise — any damage is contained inside the container and limited to the /Projects tree.

What It Does NOT Protect Against

Being honest about the limitations:

  • devbox can reach /Secrets by absolute path — Distrobox shares the host filesystem. This is why the one rule (never vibe-code in devbox) matters.
  • Packages that cause damage at import time, not install timeignore-scripts only blocks install scripts. A package that runs malicious code when you require() it is not blocked by this. Unknown packages should still go through testbox.
  • A running app using its own secrets — if the app's code itself is compromised, no container prevents it from using the .env values it was given. Container isolation stops exfiltration via the filesystem, not via an app that's already been handed the credentials.

Lessons from Building It

A few things that weren't obvious going in:

Podman can't bind-mount over a symlink. My initial approach was to mount each .env file directly. Podman silently fails (conmon JSON parse error) when you try to mount over a symlink. The fix is to mount the parent directory at its absolute path, which lets symlink resolution happen normally inside the container.

AntiGravity must run as root inside the container and needs specific flags. Electron apps refuse to launch as root without --no-sandbox. AntiGravity specifically checks for --user-data-dir in the launch args. GPU acceleration causes X11 buffer freezes inside containers, so --disable-gpu is needed. The /dev/shm inside containers is too small for Chrome's renderer — --disable-dev-shm-usage switches it to /tmp.

X11 window resize freezes are fixed with --ipc=host and LIBGL_ALWAYS_SOFTWARE=1. Without shared IPC namespace, resizing the AntiGravity window causes it to freeze. Software OpenGL rendering prevents a related GPU buffer freeze. Both flags are in the vibebox.sh launch script.

Ctrl+Z will haunt you. Pressing Ctrl+Z inside the container stops the dev server process without releasing the port. The next pnpm dev fails because the port is still held. I wrote a pdev shell function that kills whatever process is holding port 3000 before starting a new server — use Ctrl+C to stop dev servers, not Ctrl+Z.

Frequently Asked Questions

Why Podman for vibebox instead of Docker?

Rootless Podman runs containers without a daemon and without root privileges on the host. This matters for a security-focused setup — Docker's daemon model gives containers a path to escalate host privileges via the socket. Distrobox (used for devbox) also works with both, but defaults to Podman on Fedora/Ubuntu.

Can I use this setup on macOS or Windows?

The core concepts apply anywhere, but the implementation is Linux-specific. Distrobox requires a Linux kernel. Podman rootless works natively on Linux but runs in a VM on Mac/Windows, which changes the performance and X11 forwarding story significantly. On Mac, something like OrbStack with a similar isolation model would be the closest equivalent.

Does ignore-scripts=true break legitimate packages?

Occasionally, yes. Some packages have legitimate postinstall scripts (generating native bindings, writing config files, etc.). When a legitimate package fails to set up correctly, you'll usually see an error at import time. The fix is to run that specific package's postinstall manually after inspecting it: npm rebuild <package-name>. In practice I've found it breaks maybe 1 in 30 packages, and the security tradeoff is worth it.

Is there performance overhead from running inside a container?

No meaningful overhead for development workloads. Podman on Linux uses kernel namespaces and cgroups — the same technology Docker uses. There's no hypervisor layer. File I/O into /Projects is native speed (bind mount). The only noticeable cost is container startup time, which is a few seconds.

How do I handle tools that need to write outside /Projects?

Anything that needs to persist across container restarts gets its own volume mount in the vibebox launch script. The config persistence table above covers the common cases (Claude, Gemini, IDE settings). For anything else, I evaluate whether it actually needs to persist or whether it can just be re-authenticated when needed.