AI Agent Environments
Setting up disposable SSH environments for Claude Code, Cursor, Codex, and other AI coding agents
AI coding agents need disposable environments they can SSH into, run code, and throw away. They can't install desktop apps, navigate web UIs, or open IDEs. They can SSH.
Podspawn is built for this use case. An agent SSHes in, gets a container with the repo, dependencies, and services running, does its work, exits, and the container self-destructs. No cleanup, no zombie processes, no billing surprises.
Why SSH-first matters for agents
Every competitor in this space requires some combination of: a CLI tool installed on the agent's machine, a web UI interaction, an API call to a proprietary service, or a specific IDE integration. None of that works for headless agents.
SSH is the universal interface. Every agent framework supports it. Every CI system supports it. Every programming language has SSH libraries. Podspawn turns that into a container platform.
# Agent receives a task
ssh agent-run-4829@backend.pod
# Agent is in a container with the repo, deps, and services
# Write code, run tests, commit, push
exit
# Container and all companion services self-destructDestroy-on-disconnect mode
For agent workflows, you want zero grace period. The container should die the instant the SSH connection closes.
Set the session mode in /etc/podspawn/config.yaml:
session:
mode: "destroy-on-disconnect"
grace_period: "0s"
max_lifetime: "2h" # safety net for hung agentsWith destroy-on-disconnect:
- Container is destroyed immediately when the last SSH connection closes
- No grace period, no reconnect window
- Companion services (databases, caches) are destroyed too
max_lifetimeacts as a safety net for agents that hang or forget to exit
Setting up agent users
Register a dedicated user for each agent (or agent run):
# Static agent user with a persistent key
sudo podspawn add-user claude-agent --key-file /path/to/agent-key.pub
# Or generate per-run users programmatically
sudo podspawn add-user "agent-run-${RUN_ID}" --key "ssh-ed25519 AAAA..."For ephemeral per-run users, clean up the key file after the run:
# After the agent finishes
rm /etc/podspawn/keys/agent-run-${RUN_ID}Podfiles for agent workflows
A Podfile defines exactly what the agent needs. Commit it to your repo and every agent run gets the same environment.
# podfile.yaml
base: ubuntu:24.04
packages:
- nodejs@22
- python@3.12
- git
- curl
shell: /bin/bash
repos:
- url: github.com/company/backend
path: /workspace/backend
branch: main
env:
CI: "true"
DATABASE_URL: "postgres://postgres:devpass@postgres:5432/dev"
services:
- name: postgres
image: postgres:16
ports: [5432]
env:
POSTGRES_PASSWORD: devpass
POSTGRES_DB: dev
on_create: |
cd /workspace/backend && npm install
on_start: |
echo "Environment ready"When the agent SSHes in, it gets:
- The repo cloned at
/workspace/backendwith dependencies installed - A postgres instance running and reachable at
postgres:5432 - Environment variables pre-configured
Claude Code
Claude Code connects via SSH and needs a shell, git, and the project's toolchain.
Setup
-
Generate a key pair for the agent:
ssh-keygen -t ed25519 -f /path/to/claude-agent-key -N "" -
Register the user:
sudo podspawn add-user claude-agent --key-file /path/to/claude-agent-key.pub -
Configure the agent with the SSH connection:
Host: yourserver.com User: claude-agent Key: /path/to/claude-agent-key -
Claude Code SSHes in and gets a container with the project environment.
Recommended Podfile additions for Claude Code
packages:
- git
- curl
- ripgrep # Claude Code uses rg for code search
- tree # directory structure inspection
env:
EDITOR: "cat" # prevents interactive editor promptsCursor
Cursor uses VS Code Remote SSH under the hood. It connects via SSH, syncs files via SFTP, and runs commands via exec channels. All of this works with podspawn.
Setup
-
Register the user:
sudo podspawn add-user cursor-agent --key-file /path/to/cursor-key.pub -
In Cursor's SSH config, point to the podspawn server:
Host dev-env HostName yourserver.com User cursor-agent IdentityFile /path/to/cursor-key -
Connect via Remote SSH. Cursor installs its server-side component automatically.
With destroy-on-disconnect, Cursor's server component will be re-installed on each connection since the container is fresh. If this is too slow for your workflow, use grace-period mode with a short window (e.g., 300s) to keep the container alive between reconnects.
Codex
OpenAI's Codex agent operates similarly. It needs SSH access and a shell.
Setup
- Register the user and generate keys (same as above)
- Point the Codex agent configuration at the SSH endpoint
- The agent SSHes in, gets a container, does its work, and exits
Per-run isolation
For maximum isolation between agent runs, use per-run usernames:
#!/bin/bash
# orchestrator script
RUN_ID=$(uuidgen | head -c 8)
USERNAME="agent-${RUN_ID}"
# Generate ephemeral key pair
ssh-keygen -t ed25519 -f "/tmp/${USERNAME}-key" -N "" -q
# Register the user
sudo podspawn add-user "${USERNAME}" --key-file "/tmp/${USERNAME}-key.pub"
# Run the agent
ssh -i "/tmp/${USERNAME}-key" "${USERNAME}@yourserver.com" << 'SCRIPT'
cd /workspace
npm test
SCRIPT
# Cleanup
rm -f "/tmp/${USERNAME}-key" "/tmp/${USERNAME}-key.pub"
sudo rm -f "/etc/podspawn/keys/${USERNAME}"Each run gets a completely fresh container. No state leaks between runs. The key and user are cleaned up after.
Resource limits for agents
Agents can be resource-hungry. Set limits to prevent any single agent from starving others:
# /etc/podspawn/config.yaml
defaults:
cpus: 2.0
memory: "4g"
resources:
max_containers: 50
max_per_user: 3
security:
pids_limit: 512For specific agent users that need more resources, use per-user overrides:
# /etc/podspawn/users/claude-agent.yaml
cpus: 4.0
memory: "8g"Monitoring agent usage
Use podspawn list and podspawn status to monitor active agent sessions:
# See what's running
podspawn list
# System-level metrics
podspawn status
# Prometheus metrics for alerting
podspawn status --prometheusSet max_lifetime as a safety net. If an agent hangs, the container will be destroyed when the lifetime expires:
session:
max_lifetime: "2h"The cleanup daemon enforces this:
podspawn cleanup --daemon --interval 30s