{"id":137605,"date":"2026-06-15T08:00:00","date_gmt":"2026-06-15T08:00:00","guid":{"rendered":"https:\/\/fedoramagazine.org\/?p=43429"},"modified":"2026-06-15T08:00:00","modified_gmt":"2026-06-15T08:00:00","slug":"sandbox-ai-coding-agents-with-microvms-on-fedora-linux","status":"publish","type":"post","link":"https:\/\/sickgaming.net\/blog\/2026\/06\/15\/sandbox-ai-coding-agents-with-microvms-on-fedora-linux\/","title":{"rendered":"Sandbox AI coding agents with microVMs on Fedora Linux"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" width=\"300\" height=\"127\" src=\"https:\/\/sickgaming.net\/blog\/wp-content\/uploads\/2026\/06\/sandbox-ai-coding-agents-with-microvms-on-fedora-linux.jpg\" class=\"webfeedsFeaturedVisual wp-post-image\" alt=\"\" style=\"margin: auto;margin-bottom: 5px;max-width: 100%\" \/><\/p>\n<p>AI coding agents such as Claude code or Codex get more capable every month. This is great for productivity, but approving all commands gets annoying really quickly. On the other hand, allowing agents to run any command on your work machine is not a great idea. They are really good at exploring your production cluster using kubectl or running remote commands at your production servers over SSH.<\/p>\n<p>Fortunately, Linux distributions come with plenty of options for process isolation. You can run agents as a completely different user, in a container, or in a VM. This article shows how to use microVMs to run coding agents.<\/p>\n<p> <span id=\"more-43429\"><\/span> <\/p>\n<h2 class=\"wp-block-heading\">Security concerns<\/h2>\n<p>Running AI agents in unattended mode is like running untrusted code. Companies behind these agents, such as Anthropic or Google, are not trying to steal credentials, but people keep coming up with new attack vectors like <a href=\"https:\/\/en.wikipedia.org\/wiki\/Slopsquatting\" target=\"_blank\" rel=\"noreferrer noopener\">Slopsquatting<\/a> or prompt injections <a href=\"https:\/\/snyk.io\/blog\/cline-supply-chain-attack-prompt-injection-github-actions\/\" target=\"_blank\" rel=\"noreferrer noopener\">virtually anywhere<\/a>.<\/p>\n<p>The coding agents themselves ship with built-in mitigations that try to refuse prompt injections as described, for example, <a href=\"https:\/\/blog.tomecek.net\/post\/claude-code-prompt-injection-feb-2026\/\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/p>\n<p><a href=\"https:\/\/code.claude.com\/docs\/en\/sandboxing\" target=\"_blank\" rel=\"noreferrer noopener\">Lightweight sandboxing<\/a> technologies are another layer of defense in coding agents. On Linux, <a href=\"https:\/\/github.com\/containers\/bubblewrap\" target=\"_blank\" rel=\"noreferrer noopener\">bwrap<\/a> is one of the possible implementations. This raises the bar, yet sandbox escapes are still a problem. Take a look at <a href=\"https:\/\/www.sentinelone.com\/vulnerability-database\/cve-2026-39861\/\" target=\"_blank\" rel=\"noreferrer noopener\">CVE-2026-39861<\/a> as an example of multi-platform sandbox escape.<\/p>\n<p>You could use containers to isolate the agent in their own namespace, but they still share the host kernel. Some of the the recent kernel vulnerabilities resulted in <a href=\"https:\/\/fedoramagazine.org\/how-fedora-is-responding-to-recent-kernel-vulnerabilities\/\" target=\"_blank\" rel=\"noreferrer noopener\">privilege escalation<\/a> (switching from regular user to root) suggesting that containers are not enough as a <a href=\"https:\/\/emirb.github.io\/blog\/microvm-2026\/\" target=\"_blank\" rel=\"noreferrer noopener\">security boundary<\/a>.<\/p>\n<p>In the rest of this article, I describe how to use microVMs to easily sandbox coding agents on your Fedora Linux.<\/p>\n<h2 class=\"wp-block-heading\">Exploring microVMs<\/h2>\n<p>First of all, let\u2019s take a look at what microVMs are. Just like any VM, they have their own kernel, one per each microVM. Compared to traditional VMs they start in very short time (hundreds of milliseconds) but don\u2019t offer <a href=\"https:\/\/www.qemu.org\/docs\/master\/system\/i386\/microvm.html\" target=\"_blank\" rel=\"noreferrer noopener\">all the features<\/a> of full VMs.<\/p>\n<p>This article explains usage of <a href=\"https:\/\/github.com\/containers\/libkrun\" target=\"_blank\" rel=\"noreferrer noopener\">krun<\/a> runtime for podman. This approach offers the same well-known workflow as containers, but simply runs every container as a microVM.<\/p>\n<p>Start by installing the runtime:<\/p>\n<pre class=\"wp-block-preformatted\">dnf install crun-krun<\/pre>\n<p>To run a microVM, simply run <em>podman<\/em> with <em>&#8211;runtime=krun<\/em> in your terminal:<\/p>\n<pre class=\"wp-block-preformatted\">podman run --runtime=krun --rm -it fedora:44 \/bin\/bash\n<\/pre>\n<h2 class=\"wp-block-heading\">Things to watch out for<\/h2>\n<p>A microVM is not a regular container, so a few things might behave differently. First, allocate enough CPU and RAM with krun annotations. The defaults are too small and might result in OOM (Out Of Memory) kills. Second, make sure you have libkrun version &gt;= 1.8. Older versions have a <a href=\"https:\/\/github.com\/containers\/podman\/issues\/28067\" target=\"_blank\" rel=\"noreferrer noopener\">bug<\/a> which prevents you from pressing Enter in your coding agent. Third, the microVM ignores the USER set in the Dockerfile and always boots as root. Either switch to the correct user manually or put the switch into an entrypoint script.<\/p>\n<h2 class=\"wp-block-heading\">Case study: sandboxing Claude Code for a Python project<\/h2>\n<p>This section outlines a simple setup for a Python project managed by <a href=\"https:\/\/docs.astral.sh\/uv\/\" target=\"_blank\" rel=\"noreferrer noopener\">uv<\/a>. It uses podman-compose to mount the project into the microVM. Compared to containers, this podman compose needs additional annotations for UID\/GID translation, SELinux labeling, and HW resources. The final setup is very similar to what you would need for containers.<\/p>\n<p>To install podman compose from official Fedora repositories, run:<\/p>\n<pre class=\"wp-block-preformatted\">dnf install podman-compose<\/pre>\n<p>The setup has 3 parts:<\/p>\n<ul class=\"wp-block-list\">\n<li>Dockerfile<\/li>\n<li>docker-compose.yaml<\/li>\n<li>entrypoint.sh<\/li>\n<\/ul>\n<h3 class=\"wp-block-heading\">Dockerfile<\/h3>\n<p>As mentioned above, podman with krun runtime still runs containers, but spawns each of them in a microVM. This example container includes uv package manager, claude code and a few additional RPM packages. Define your own container based on your project dependencies and programming language.<\/p>\n<p>Make sure to create an unprivileged user and use it for running the agent.<\/p>\n<pre class=\"wp-block-preformatted\">FROM fedora:44 ARG HOST_UID=1000\nARG HOST_GID=1000 # Create group and user matching host UID\/GID\nRUN groupadd -g ${HOST_GID} appuser &amp;&amp; \\ useradd -u ${HOST_UID} -g ${HOST_GID} -m appuser RUN mkdir -p \/venv &amp;&amp; chown appuser:appuser \/venv\nRUN mkdir -p \/home\/appuser\/.claude &amp;&amp; chown appuser:appuser \/home\/appuser\/.claude USER appuser # Rarely-changing tooling. Kept above the dnf layer so editing the RPM list\n# below does not invalidate (and re-run) these installs.\nRUN curl -LsSf https:\/\/astral.sh\/uv\/install.sh | sh &amp;&amp; \\ curl -fsSL https:\/\/claude.ai\/install.sh | bash\nUSER root # Frequently-changing RPMs. Kept last so adding a package only rebuilds from here down.\nRUN dnf install git make vim free libpq-devel python3-devel gcc -y &amp;&amp; \\ dnf clean all COPY --chown=appuser entrypoint.sh \/entrypoint.sh\nRUN chmod +x \/entrypoint.sh USER appuser\nWORKDIR \/app # This is needed because entrypoint does not have .local\/bin in the PATH\nENV PATH=\"\/home\/appuser\/.local\/bin:$PATH\"\nENTRYPOINT [\"\/entrypoint.sh\"]\nCMD [\"\/bin\/bash\"]<\/pre>\n<h3 class=\"wp-block-heading\">docker-compose.yaml<\/h3>\n<p>The compose file defines how to mount the project directory into the microVM. This is where most of the magic happens, because podman needs to translate UID\/GID and manage SELinux labels, otherwise the files would not be accessible inside of the microVM or they would end up being owned by a different user.<\/p>\n<pre class=\"wp-block-preformatted\">services: claude: container_name: project-name-claude annotations: run.oci.handler: krun krun.ram_mib: \"4096\" krun.cpus: \"4\" user: \"${HOST_UID}:${HOST_GID}\" userns_mode: keep-id # optional, for rootless host build: context: . args: HOST_UID: \"${HOST_UID}\" # use UID and GID from the host so that files created in the container have correct permissions HOST_GID: \"${HOST_GID}\" volumes: - ..\/:\/app:U,z # bind mount your project - project-name-venv-cache:\/venv:U,z - claude-config:\/home\/appuser\/.claude:U,z # persistent claude credentials\/config working_dir: \/app stdin_open: true tty: true environment: - CLAUDE_CONFIG_DIR=\/home\/appuser\/.claude - UV_LINK_MODE=copy - UV_PROJECT_ENVIRONMENT=\/venv\/env # This is inside the cached volume - UV_PYTHON_INSTALL_DIR=\/venv\/python # So that uv-managed python installations are not in home but cached in \/venv - TERM=xterm-256color - COLORTERM=truecolor volumes: project-name-venv-cache: claude-config: external: true name: claude-config<\/pre>\n<p>There are 3 key parts:<\/p>\n<ul class=\"wp-block-list\">\n<li>annotations \u2013 these select krun as a runtime and specify HW requirements<\/li>\n<li><a href=\"https:\/\/www.redhat.com\/en\/blog\/rootless-podman-user-namespace-modes\" target=\"_blank\" rel=\"noreferrer noopener\">user and userns_mode<\/a> \u2013 this tells podman to translate UID\/GID so that the files created in the microVM end up owned by your user on the host<\/li>\n<li><a href=\"https:\/\/oneuptime.com\/blog\/post\/2026-03-17-selinux-volume-options-podman\/view\" target=\"_blank\" rel=\"noreferrer noopener\">volume labels<\/a> \u2013 z tells podman to relabel the files with a shared SELinux label. Otherwise SELinux would prevent the process inside the microVM from touching the files in the volume. U tells podman to recursively chown all files.<\/li>\n<\/ul>\n<h3 class=\"wp-block-heading\">entrypoint.sh<\/h3>\n<p>The entrypoint creates a virtual environment for the Python project, because we don\u2019t want dynamic dependencies baked into the container image. It also runs the switch from root to regular user because podman with krun runtime ignores the USER directive from the container.<\/p>\n<pre class=\"wp-block-preformatted\">#!\/bin\/bash\nset -e echo \"Sandbox started as user: $(id -un) in directory: $(pwd)\" if [ \"$(id -un)\" != \"appuser\" ]; then runuser -u appuser -- uv sync echo \"Running ${@} as appuser\" exec runuser -u appuser -- \"$@\"\nfi uv sync\nexec \"$@\"\n<\/pre>\n<h3 class=\"wp-block-heading\">Run the setup<\/h3>\n<p>First, build the container:<\/p>\n<pre class=\"wp-block-preformatted\">$ HOST_UID=$(id -u) HOST_GID=$(id -g) podman-compose -f .agent-sandbox\/docker-compose.yaml build\nSTEP 1\/18: FROM fedora:44\n...\nSuccessfully tagged localhost\/agent-sandbox_claude:latest<\/pre>\n<p>Then create the external volume and run the claude container interactively:<\/p>\n<pre class=\"wp-block-preformatted\">$ podman volume create claude-config\n$ HOST_UID=$(id -u) HOST_GID=$(id -g) podman-compose -f .agent-sandbox\/docker-compose.yaml run --rm claude\nSandbox started as user: root in directory: \/app\nResolved 3 packages in 6ms\nChecked 3 packages in 1ms\nRunning \/bin\/bash as appuser\ntty: ttyname error: Inappropriate ioctl for device\n[appuser@3bd1234b9a77 app]$<\/pre>\n<p>You can now check that the kernel is different by running <kbd>uname -a <\/kbd>inside of the microVM.<\/p>\n<h2 class=\"wp-block-heading\">Putting it together: single script to create the whole setup<\/h2>\n<p>Creating the same setup manually for every project is not the greatest user experience, but you can automate the setup using a simple script like <a href=\"https:\/\/github.com\/msehnout\/agent-sandbox\" target=\"_blank\" rel=\"noreferrer noopener\">this<\/a>. It installs a new <kbd>sbx<\/kbd> command that wraps the setup described above into 3 simple command options: init, build, and run.<\/p>\n<h2 class=\"wp-block-heading\">A word of caution \u2014 microVM is not a bulletproof boundary<\/h2>\n<p>A microVM raises the bar considerably, but it is not perfect isolation, and it would be irresponsible to present it as such. Take a look at the <a href=\"https:\/\/github.com\/containers\/libkrun\" target=\"_blank\" rel=\"noreferrer noopener\">libkrun git repository<\/a> to read more about the <a href=\"https:\/\/github.com\/containers\/libkrun\/discussions\/538\" target=\"_blank\" rel=\"noreferrer noopener\">security model<\/a>.<\/p>\n<p>If you want to run software that might be dangerous, prefer using a full VM or even cloud VM.<\/p>\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n<p>MicroVMs seem like a sweet spot for running AI Agents. They provide a familiar workflow of containers, but the agents run on their own kernel behind a hypervisor. This article describes workflow based on podman and krun runtime because Fedora Linux ships both of them natively, but there are plenty of other options available for any platform (for example <a href=\"https:\/\/www.docker.com\/products\/docker-sandboxes\/\">docker<\/a><a href=\"https:\/\/www.docker.com\/products\/docker-sandboxes\/\" target=\"_blank\" rel=\"noreferrer noopener\">sandbox<\/a>).<\/p>\n<p><em><strong>Note about AI usage: <\/strong>I wrote this article myself. I used Claude (Anthropic) to significantly refine the grammar, wording, and sentence structure; the technical content and all claims are my own and tested.<\/em><\/p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>AI coding agents such as Claude code or Codex get more capable every month. This is great for productivity, but approving all commands gets annoying really quickly. On the other hand, allowing agents to run any command on your work machine is not a great idea. They are really good at exploring your production cluster [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":137606,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[48],"tags":[45,61,46,47],"class_list":["post-137605","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-fedora-os","tag-fedora","tag-fedora-project-community","tag-magazine","tag-news"],"_links":{"self":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts\/137605","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/comments?post=137605"}],"version-history":[{"count":0,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/posts\/137605\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/media\/137606"}],"wp:attachment":[{"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/media?parent=137605"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/categories?post=137605"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sickgaming.net\/blog\/wp-json\/wp\/v2\/tags?post=137605"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}