Deploying Autonomous AI Agents
Section titled “Deploying Autonomous AI Agents”An AI agent framework on its own gives the agent no way to act. The model can generate code but has nowhere to run it, can plan a deployment but cannot execute one, and can describe a test without being able to open a browser. A human has to carry out every step it proposes.
A Hoody container closes that gap.
Every service in a container (terminal, filesystem, database, browser, display, process manager) is an HTTPS endpoint, so an agent needs no special adapter, plugin, or permission negotiation: it makes HTTP requests, which agents already know how to do. Every process the agent spawns gets a URL, and every tool it uses is one. The built-in hoody-agent service adds a single conversational interface over all of it, so the agent can manage itself.
That is the design rather than a coincidence: infrastructure that exposes everything over HTTP is infrastructure an AI can operate directly. And it all runs on servers you own, every agent with a complete machine of its own.
Agent capabilities over HTTP
Section titled “Agent capabilities over HTTP”Consider what an autonomous agent needs:
| Capability | Traditional stack | Hoody |
|---|---|---|
| Execute commands | SSH + credentials + firewall rules | POST /api/v1/terminal/execute on terminal-1 |
| Read/write files | SFTP + mount points + permissions | GET/PUT /api/v1/files/... on files-1 |
| Query databases | Connection strings + drivers + ORM | POST /api/v1/sqlite/db on sqlite-1 |
| Automate browsers | Puppeteer setup + Chromium install | GET /screenshot, GET /browse on browser-1 |
| Operate GUI applications | VNC server + desktop automation scripts | GET /api/v1/display/screenshot, POST /api/v1/display/input/act on display-1 |
| Spawn more agents | Infrastructure provisioning | POST /api/v1/projects/{id}/containers |
| Observe everything | Logging infrastructure | Every HTTP request is observable |
Every capability is one HTTP call. There is no SDK to install and no driver to manage. Container service calls are authorized at the Hoody Proxy: with no permissions configured the container URL itself is the credential, and where a permission group is configured the proxy checks the client IP range, HTTP Basic credentials, JWT claims, or a bearer token. Management API calls on api.hoody.com carry your Hoody API bearer token.
Step 1: Create an agent container
Section titled “Step 1: Create an agent container”Give your AI agent its own computer:
# Create a container for your agenthoody containers create --project $PROJECT_ID \ --server-id $SERVER_ID \ --name "ai-agent-alpha" \ --container-image "debian/13" \ --hoody-kitimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const agent = await client.api.containers.create(PROJECT_ID, { name: 'ai-agent-alpha', server_id: SERVER_ID, container_image: 'debian/13', hoody_kit: true,});
console.log('Agent container:', agent.data.id);// Agent now has: terminal, files, sqlite, browser,// exec, display, code, daemon, cron, notifications...curl -X POST "https://api.hoody.com/api/v1/projects/$PROJECT_ID/containers" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "ai-agent-alpha", "server_id": "'$SERVER_ID'", "container_image": "debian/13", "hoody_kit": true }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Creates the agent’s container with Hoody Kit enabled, the same request as the CLI and SDK tabs above.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/projects/PROJECT_ID/containers&method=POST&bearer_token=TOKEN&json={"name":"ai-agent-alpha","server_id":"SERVER_ID","container_image":"debian/13","hoody_kit":true}&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
That container is a complete computer. The agent has all 19 HTTP services at its disposal without further setup.
Step 2: Drive hoody-agent over HTTP
Section titled “Step 2: Drive hoody-agent over HTTP”The hoody-agent service is a full autonomous coding agent accessible entirely through HTTP, across 100+ endpoints:
# The `hoody agent` CLI has subcommands (sessions, tools, ...). Prompts run via `hoody agent sessions turns run` / `hoody agent sessions turns start --stream`.# You can also drive it directly via HTTP to the agent service URL.# Prompts are session-scoped: create a session first, then dispatch turns into it.SESSION_ID=$(curl -s -X POST "https://$PROJECT_ID-$AGENT_ID-agent-1.$SERVER.containers.hoody.com/api/v1/agent/sessions" \ -H "Content-Type: application/json" -d '{}' | jq -r '.session_id')
# ?policy=auto_approve adopts the headless posture so confirm gates auto-approve.curl -X POST "https://$PROJECT_ID-$AGENT_ID-agent-1.$SERVER.containers.hoody.com/api/v1/agent/sessions/$SESSION_ID/prompt:sync?policy=auto_approve" \ -H "Content-Type: application/json" \ -d '{ "text": "Set up a Node.js REST API with user authentication, SQLite database, and automated tests. Deploy it as a daemon process." }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Using raw fetch to show the HTTP surface directly.// The same endpoints are also available via client.agent.* in the SDK.// Prompts are session-scoped: create a session first, then dispatch turns into it.const agentBase = `https://${PROJECT_ID}-${AGENT_ID}-agent-1.${SERVER}.containers.hoody.com`;
// The daemon returns the session id as session_id at the top level.const session = await fetch(`${agentBase}/api/v1/agent/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),}).then((r) => r.json());
const response = await fetch( // ?policy=auto_approve adopts the headless posture so confirm gates auto-approve. `${agentBase}/api/v1/agent/sessions/${session.session_id}/prompt:sync?policy=auto_approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: `Set up a Node.js REST API with user authentication, SQLite database, and automated tests. Deploy it as a daemon process.`, }), });
const result = await response.json();console.log('Session ID:', session.session_id);// Agent now autonomously:// 1. Installs Node.js via terminal// 2. Writes API code via file operations// 3. Creates database schema via SQLite// 4. Runs tests via terminal// 5. Starts daemon via daemon manager# Create a session, then dispatch a blocking turn into it.SESSION_ID=$(curl -s -X POST "https://$PROJECT_ID-$AGENT_ID-agent-1.$SERVER.containers.hoody.com/api/v1/agent/sessions" \ -H "Content-Type: application/json" -d '{}' | jq -r '.session_id')
# ?policy=auto_approve adopts the headless posture so confirm gates auto-approve.curl -X POST "https://$PROJECT_ID-$AGENT_ID-agent-1.$SERVER.containers.hoody.com/api/v1/agent/sessions/$SESSION_ID/prompt:sync?policy=auto_approve" \ -H "Content-Type: application/json" \ -d '{ "text": "Set up a Node.js REST API with user authentication, SQLite database, and automated tests. Deploy it as a daemon process." }'One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Opens a session, then dispatches the build task into it and waits for the turn to end. It returns early with a pending_gate when the agent asks a question or proposes a plan, and after about five minutes answers 503 with details.turn_id while the turn keeps running; follow it with GET /api/v1/agent/sessions/SESSION_ID/turns/TURN_ID. Copy the session_id from the first response into SESSION_ID before running the second link; policy=auto_approve lets the agent proceed through confirm gates unattended.
# Create session
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-AGENT_ID-agent-1.SERVER.containers.hoody.com/api/v1/agent/sessions&method=POST&json={}&response=transparent
# Send prompt
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-AGENT_ID-agent-1.SERVER.containers.hoody.com/api/v1/agent/sessions/SESSION_ID/prompt:sync?policy=auto_approve&method=POST&json={"text":"Set%20up%20a%20Node.js%20REST%20API%20with%20user%20authentication,%20SQLite%20database,%20and%20automated%20tests.%20Deploy%20it%20as%20a%20daemon%20process."}&response=transparent The agent now works autonomously. It reads files, writes code, executes commands, queries databases, and deploys services, all through the same HTTP services available to any human user.
Live session output
Section titled “Live session output”Stream agent output in real time with the non-sync prompt endpoint, which returns Server-Sent Events:
// Stream real-time updates via SSE (session-scoped: dispatch into an existing session)const response = await fetch( // ?policy=auto_approve adopts the headless posture so confirm gates auto-approve. `https://${PROJECT_ID}-${AGENT_ID}-agent-1.${SERVER}.containers.hoody.com/api/v1/agent/sessions/${SESSION_ID}/prompt:stream?policy=auto_approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'Run the test suite and report results', }), });
const reader = response.body.getReader();const decoder = new TextDecoder();while (true) { const { done, value } = await reader.read(); if (done) break; console.log(decoder.decode(value)); // Streams tool calls, tool results, and assistant messages as they happen}Or list the agent’s sessions, then tail one’s live event stream:
# List sessionshttps://PROJECT_ID-AGENT_ID-agent-1.SERVER.containers.hoody.com/api/v1/agent/sessions
# Live event stream for one sessionhttps://PROJECT_ID-AGENT_ID-agent-1.SERVER.containers.hoody.com/api/v1/agent/sessions/SESSION_ID/streamYou can also reach the agent directly. Its HTTP API, with browsable OpenAPI docs, lives at the agent service URL, and you can drive it interactively through the hoody CLI, the container’s web terminal, or SSH:
https://PROJECT_ID-AGENT_ID-agent-1.SERVER.containers.hoody.comStep 3: Give the agent full access
Section titled “Step 3: Give the agent full access”An agent in a Hoody container has access to everything a human developer would:
Terminal access
Section titled “Terminal access”# Agent can execute any shell command.# ephemeral=true gives an isolated one-shot PTY. Send it to the terminal-0 hostname: on any terminal-N hostname the proxy forces terminal_id=N and ephemeral can no longer auto-generate an id.curl -X POST "https://$PROJECT_ID-$AGENT_ID-terminal-0.$SERVER.containers.hoody.com/api/v1/terminal/execute?ephemeral=true" \ -H "Content-Type: application/json" \ -d '{"command": "git clone https://github.com/user/repo && cd repo && npm install && npm test"}'Filesystem access
Section titled “Filesystem access”# Agent can read and write files under the served root; protected paths stay inaccessible. Upload is PUT /api/v1/files/{path} with the file content as the bodycurl -X PUT "https://$PROJECT_ID-$AGENT_ID-files-1.$SERVER.containers.hoody.com/api/v1/files/app/config.json" \ -H "Content-Type: application/octet-stream" \ --data-binary '{"port": 3000, "env": "production"}'Database access
Section titled “Database access”# Agent can query and modify databases; the `db` query parameter is requiredcurl -X POST "https://$PROJECT_ID-$AGENT_ID-sqlite-1.$SERVER.containers.hoody.com/api/v1/sqlite/db?db=/hoody/databases/app.db&create_db_if_missing=true" \ -H "Content-Type: application/json" \ -d '{"transaction": [{"query": "CREATE TABLE metrics (id INTEGER PRIMARY KEY, name TEXT, value REAL, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)"}]}'Browser automation
Section titled “Browser automation”# Agent can navigate websites, take screenshots, interact with UI.# /screenshot navigates to the URL and captures it in one call (PNG by default).# The browser-1 hostname selects the instance. Instances auto-start by default, so this# call starts one if none is running; where auto-start is disabled, pass start=true.curl "https://$PROJECT_ID-$AGENT_ID-browser-1.$SERVER.containers.hoody.com/api/v1/browser/screenshot?url=https://example.com" \ --output page.pngMulti-agent orchestration
Section titled “Multi-agent orchestration”Because containers are peers connected by HTTP, agents can orchestrate other agents.
Orchestrator and worker agents
Section titled “Orchestrator and worker agents”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Agent A: The Orchestrator// Creates a specialized container for Agent Bconst workerContainer = await client.api.containers.create(PROJECT_ID, { name: 'ai-worker-backend', server_id: SERVER_ID, container_image: 'debian/13', hoody_kit: true,});
// Agent A assigns a task to Agent B via the agent service URL.// Prompts are session-scoped: open a session on Agent B, then dispatch a turn.// prompt:sync returns when the turn ends, when it parks at a gate, or after about five// minutes with a 503 (details.turn_running) while the turn keeps running.const workerBase = `https://${PROJECT_ID}-${workerContainer.data.id}-agent-1.${SERVER}.containers.hoody.com`;
const workerSession = await fetch(`${workerBase}/api/v1/agent/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),}).then((r) => r.json());
const taskResponse = await fetch( // ?policy=auto_approve adopts the headless posture so confirm gates auto-approve. `${workerBase}/api/v1/agent/sessions/${workerSession.session_id}/prompt:sync?policy=auto_approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'Implement the payment processing module with Stripe integration. Write tests. Do not deploy until all tests pass.', }), });const result = await taskResponse.json();// prompt:sync returns either a final result.status ('done', 'error', 'canceled', or 'quit')// or a pending_gate: auto_approve answers confirm gates, but a question or a proposed plan// still parks the turn until you answer it. A 503 with details.turn_running means the turn// outlived the request: poll GET /api/v1/agent/sessions/{id}/turns/{turn_id}.
// Agent A inspects Agent B's work by reading its files directly via kit servicesconst containerClient = await client.withContainer({ id: workerContainer.data.id, project_id: PROJECT_ID, server: SERVER,});
const code = await containerClient.files.get('/app/src/payments.ts');
// Agent A runs Agent B's tests from Agent B's terminal (ephemeral one-shot PTY).// command is the request body; ephemeral is a query-string option (second arg).// serviceIndex 0 = the terminal-0 hostname, so ephemeral can auto-generateconst testResult = await containerClient.terminal.commands.run( { command: 'cd /app && bun test payments' }, { ephemeral: true }, { serviceIndex: 0 },);All of this is ordinary HTTP between containers: Agent A controls Agent B’s terminal, reads its files, and queries its database. There is no message queue and no coordinator service, only URLs calling URLs.
The floating architecture in practice
Section titled “The floating architecture in practice”┌─────────────────────┐│ ORCHESTRATOR (A) ││ Plans architecture ││ Assigns tasks ││ Reviews work │└──────────┬──────────┘ │ HTTP ┌─────┴─────┐ v v┌──────────┐ ┌──────────┐│ WORKER B │ │ WORKER C ││ Backend │ │ Frontend ││ code │ │ code │└──────────┘ └──────────┘ │ │ │ HTTP │ HTTP v v┌──────────┐ ┌──────────┐│ WORKER D │ │ WORKER E ││ Tests │ │ Design ││ & QA │ │ review │└──────────┘ └──────────┘Each worker is an isolated container with its own agent, terminal, files, and database. The orchestrator coordinates via HTTP, and workers can spawn sub-workers of their own.
MCP client integration
Section titled “MCP client integration”Hoody Agent includes an MCP (Model Context Protocol) client that connects to external MCP servers and discovers their tools at runtime. It is built on the official Go SDK and speaks every spec revision from 2024-11-05 through the current 2026-07-28, negotiating with each server automatically, over stdio, Streamable HTTP, or the legacy HTTP+SSE transport.
Servers are configured in your user, project, or local settings, and each session resolves them from those layers. You manage them without touching a file: the Agents ▸ MCP servers mode in the TUI adds, imports, enables, disables, and reconnects them, and the same surface is scriptable under /api/v1/agent/mcp/…. Paste a config block from Claude, Cursor, or VS Code and it is understood as-is; imported servers land disabled so you review before anything runs. Changes reach sessions that are already open: removing or disabling a server cuts it off immediately, mid-turn.
Add a server over HTTP. Writes are two-step: mint a single-use nonce, then present it with expect_hash, the content hash you last read, so a concurrent edit is a conflict rather than a silent overwrite. Both fields are required on every write route. Writing into a settings file that does not exist yet is no exception: the read returns the empty-array hash for a missing file, and you pass that back.
AGENT=https://$PROJECT_ID-$AGENT_ID-agent-1.$SERVER.containers.hoody.com
# 1. Read the current config, the layer files behind it, and each server's live state.# Credentials come back as key NAMES only, never values. Needs a live session.curl -s "$AGENT/api/v1/agent/mcp/servers?session_id=$SESSION_ID"
# 2. Mint the write nonce (op: upsert | delete | set_enabled | import).curl -s -X POST "$AGENT/api/v1/agent/mcp/write-intents" \ -H "Content-Type: application/json" \ -d "{\"session_id\":\"$SESSION_ID\",\"op\":\"upsert\",\"scope\":\"user\"}"
# 3. Write. Fields you omit keep their stored value. A removed or re-pointed# server is revoked in every live session before the response returns. The# named session's reconnect is awaited but not guaranteed: a server that# fails to start still returns 200, so read servers[].connected in the# reply. Other live sessions reconnect in the background.curl -s -X PUT "$AGENT/api/v1/agent/mcp/servers" \ -H "Content-Type: application/json" \ -d "{ \"session_id\": \"$SESSION_ID\", \"nonce\": \"$NONCE\", \"scope\": \"user\", \"expect_hash\": \"$HASH\", \"server\": { \"name\": \"github\", \"command\": \"docker\", \"args\": [\"run\", \"-i\", \"--rm\", \"-e\", \"GITHUB_PERSONAL_ACCESS_TOKEN\", \"ghcr.io/github/github-mcp-server\"], \"env\": { \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"\${GITHUB_TOKEN}\" } } }"POST /mcp/parse previews what a pasted document would import without writing anything, and POST /mcp/reconnect re-reads the settings layers after a hand edit. POST /mcp/probe, which tries a candidate config and reports what it offers without saving anything, is human-only and returns 403 to a machine caller, because probing starts a process or makes an outbound request to a caller-chosen URL.
Once a session is live, list the mcp__* tools it exposes:
# MCP servers are configured in agent settings; list a live session's MCP tools.# --x-hoody-realm sets the agent's realm scope ("global" or a 24-hex id); it is the# same selector the HTTP tab spells as ?realm=, and the header wins when both are sent.hoody agent sessions mcp tools list --id $SESSION_ID --x-hoody-realm global -o jsonimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Using raw fetch to show the HTTP surface directly.// The same endpoints are also available via client.agent.* in the SDK.// MCP servers are configured in agent settings; this lists the mcp__* tools a live session exposes.const mcpTools = await fetch( `https://${PROJECT_ID}-${AGENT_ID}-agent-1.${SERVER}.containers.hoody.com/api/v1/agent/sessions/${SESSION_ID}/tools/mcp`, { method: 'GET' }).then((r) => r.json());# GET the mcp__* tools available in a live session (404 if the session is not live).curl "https://$PROJECT_ID-$AGENT_ID-agent-1.$SERVER.containers.hoody.com/api/v1/agent/sessions/$SESSION_ID/tools/mcp"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Lists the mcp__* tools an already-live session exposes. Returns 404 if the session has not been opened yet.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-AGENT_ID-agent-1.SERVER.containers.hoody.com/api/v1/agent/sessions/SESSION_ID/tools/mcp&method=GET&response=transparent With MCP servers configured, the agent discovers their tools and merges them with its built-in capabilities. Connect GitHub, Slack, Jira, custom APIs, or any MCP-compatible server, all orchestrated through the same HTTP interface.
Autonomous deployment workflow
Section titled “Autonomous deployment workflow”Here is a real-world pattern: an AI agent that deploys your application end-to-end.
import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Step 1: Snapshot before deployment (safety net)// Capture the returned snapshot name for the rollback. With an alias, the// name is the alias stripped to letters, digits, _ and - (a `.` or `:` in a// timestamp alias is dropped), so always use the name the API returns.const snapshot = await client.api.snapshots.create(PRODUCTION_ID, { alias: `pre-deploy-${Date.now()}`,});const snapshotName = snapshot.data.snapshot.name;
// Step 2: Agent pulls latest code and deploys via agent service URL.// Prompts are session-scoped: open a session, then dispatch the turn.// prompt:sync waits for the turn to end, but returns early with a pending_gate for a// question or plan gate, and after about five minutes answers 503 with details.turn_id// and details.turn_running while the turn keeps running.const prodBase = `https://${PROJECT_ID}-${PRODUCTION_ID}-agent-1.${SERVER}.containers.hoody.com`;
const deploySession = await fetch(`${prodBase}/api/v1/agent/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),}).then((r) => r.json());
const deployResponse = await fetch( // ?policy=auto_approve adopts the headless posture so confirm gates auto-approve. `${prodBase}/api/v1/agent/sessions/${deploySession.session_id}/prompt:sync?policy=auto_approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: ` 1. Pull latest code from main branch 2. Install dependencies 3. Run full test suite -- stop if any test fails 4. Build production assets 5. Restart the daemon process 6. Run smoke tests against the live URL 7. Report status via notification `, }), });const result = await deployResponse.json();
// prompt:sync returns a final result.status, a pending_gate that is still waiting for an// answer, or a 503 whose details.turn_running says the turn is still going (follow it with// GET /api/v1/agent/sessions/{id}/turns/{turn_id}). Roll back only on a real failure,// never on a parked or still-running turn.const stillRunning = deployResponse.status === 503 && result.details?.turn_running;if (!result.pending_gate && !stillRunning && result.status !== 'done') { // Rollback: restore the snapshot created in Step 1 (by machine name, not alias) await client.api.snapshots.restore(PRODUCTION_ID, snapshotName);
// Notify the team via kit notifications service const containerClient = await client.withContainer({ id: PRODUCTION_ID, project_id: PROJECT_ID, server: SERVER, }); await containerClient.notifications.send({ summary: 'Deployment Failed', body: `Rolled back to pre-deploy snapshot. Status: ${result.status ?? 'unknown'}${result.text ? ` — ${result.text}` : ''}`, display: '1', });}The agent handles the entire deployment pipeline, and if anything goes wrong the snapshot restore puts its disk back, restarting the container if it was running. There is no CI/CD platform and no YAML in the loop: an agent, HTTP calls, and a snapshot to fall back on.
The snapshot-first pattern
Section titled “The snapshot-first pattern”AI agents are powerful but unpredictable, so snapshot before every run. Restore puts the container’s own disk back, restarting it if it was running; it answers 409 while the container’s network policy is still pending, so configure its network first.
# Always snapshot before letting an agent runhoody snapshots create -c $AGENT_ID \ --alias "before-agent-experiment"
# Let the agent work via direct HTTP to agent service (session-scoped)SESSION_ID=$(curl -s -X POST "https://$PROJECT_ID-$AGENT_ID-agent-1.$SERVER.containers.hoody.com/api/v1/agent/sessions" \ -H "Content-Type: application/json" -d '{}' | jq -r '.session_id')
# ?policy=auto_approve adopts the headless posture so confirm gates auto-approve.curl -X POST "https://$PROJECT_ID-$AGENT_ID-agent-1.$SERVER.containers.hoody.com/api/v1/agent/sessions/$SESSION_ID/prompt:sync?policy=auto_approve" \ -H "Content-Type: application/json" \ -d '{"text": "Refactor the entire codebase to use TypeScript 5 features"}'
# If the agent breaks something, restore by the snapshot's NAME. For an# alias of 2 to 64 letters, digits, `_` and `-` that starts and ends with# a letter or digit, the name is the alias, so you can restore directly.hoody snapshots restore -c $AGENT_ID --name "before-agent-experiment" -yimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Snapshot first, alwaysconst snapshot = await client.api.snapshots.create(AGENT_ID, { alias: 'before-agent-experiment',});const snapshotName = snapshot.data.snapshot.name;
// Let the agent work via direct HTTP to agent service.// Prompts are session-scoped: open a session, then dispatch the turn.const agentBase = `https://${PROJECT_ID}-${AGENT_ID}-agent-1.${SERVER}.containers.hoody.com`;
// The daemon returns the session id as session_id at the top level.const session = await fetch(`${agentBase}/api/v1/agent/sessions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),}).then((r) => r.json());
await fetch( // ?policy=auto_approve adopts the headless posture so confirm gates auto-approve. `${agentBase}/api/v1/agent/sessions/${session.session_id}/prompt:sync?policy=auto_approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'Refactor the entire codebase to use TypeScript 5 features', }), });
// If the agent breaks something: restore by the snapshot's machine name (not the alias)await client.api.snapshots.restore(AGENT_ID, snapshotName);# Snapshot first; capture the snapshot's machine name from the responseSNAP_NAME=$(curl -s -X POST "https://api.hoody.com/api/v1/containers/$AGENT_ID/snapshots" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "before-agent-experiment"}' | jq -r '.data.snapshot.name')
# Let the agent work (session-scoped: create a session, then dispatch the turn)SESSION_ID=$(curl -s -X POST "https://$PROJECT_ID-$AGENT_ID-agent-1.$SERVER.containers.hoody.com/api/v1/agent/sessions" \ -H "Content-Type: application/json" -d '{}' | jq -r '.session_id')
# ?policy=auto_approve adopts the headless posture so confirm gates auto-approve.curl -X POST "https://$PROJECT_ID-$AGENT_ID-agent-1.$SERVER.containers.hoody.com/api/v1/agent/sessions/$SESSION_ID/prompt:sync?policy=auto_approve" \ -H "Content-Type: application/json" \ -d '{"text": "Refactor the entire codebase to use TypeScript 5 features"}'
# If the agent breaks something, restore from the snapshot by its machine namecurl -X PUT "https://api.hoody.com/api/v1/containers/$AGENT_ID/snapshots/$SNAP_NAME" \ -H "Authorization: Bearer $HOODY_TOKEN"One request, one link
cURL runs inside your container and can wrap any HTTP request into a single GET URL. The call stops being something you need a client for and becomes something you can paste into a browser, send in a chat, bookmark, schedule with cron, or drop into a no-code tool.
Nothing is installed on the machine that opens it. The link does carry whatever credentials the call needs, so treat it as you would treat those credentials.
Slashes, colons and braces pass through as they are. The one character you must
encode is an & inside a value, which happens when the wrapped URL
carries its own query string. Left raw it ends the value early, and the rest is
read as cURL's own parameters, so you get a 200 on a request you did
not make.
How the wrapping works Chaining calls into one link Turning a link into a shortcut
Four links for the safety-net sequence: snapshot the container, open a session, dispatch the refactor task into it, and restore the pre-run snapshot if it goes wrong. Copy the snapshot.name from the first response into SNAP_NAME, and the session_id from the second response into SESSION_ID, before running the links that need them — the restore link needs the snapshot’s machine name, not its alias. Route these through a different running container’s curl-1: restoring the snapshot restarts AGENT_ID, which would take down the very service carrying this request.
# Create snapshot
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/AGENT_ID/snapshots&method=POST&bearer_token=TOKEN&json={"alias":"before-agent-experiment"}&response=transparent
# Create session
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-AGENT_ID-agent-1.SERVER.containers.hoody.com/api/v1/agent/sessions&method=POST&json={}&response=transparent
# Send prompt
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-AGENT_ID-agent-1.SERVER.containers.hoody.com/api/v1/agent/sessions/SESSION_ID/prompt:sync?policy=auto_approve&method=POST&json={"text":"Refactor%20the%20entire%20codebase%20to%20use%20TypeScript%205%20features"}&response=transparent
# Restore if it goes wrong
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/AGENT_ID/snapshots/SNAP_NAME&method=PUT&bearer_token=TOKEN&response=transparent The link carries a credential and executes with it, so it is as sensitive as the credential itself — and it passes through the cURL service's request log on the way, not just the target's. Share it only where you would share the secret, and prefer a delegated token with minimal permissions and an expiry: see API tokens.
Container isolation for experiments
Section titled “Container isolation for experiments”The danger with AI agent mistakes is not the mistake itself but how far it can propagate. Container isolation stops it at the container boundary:
- Per-agent containers: one agent cannot read another container’s filesystem or signal its processes; what connects them is what you add, such as HTTP access, a shared storage mount, or credentials
- Isolated experiments: an agent testing a destructive migration cannot touch production data that its container has no mount, network path or credential for, so keep those out of experimental containers
- Contained failures: if an agent installs malicious packages, the damage stays in its container, apart from shared storage, credentials and network access you gave it. A fork bomb is different: process and memory limits are not set by default, so it can affect the host’s capacity until you configure them
- Preserved evidence: snapshot the container after an agent run to see which files and packages changed; a disk snapshot does not record processes or network traffic, so keep logs elsewhere
Containers share the host kernel, so this containment is enforced by the kernel rather than by separate hardware; on a rented or owned server that hardware runs no other customer’s workload (free-tier slices share a host). A kernel or runtime vulnerability is the remaining path out, as for any container boundary.
External AI access through @hoody.com
Section titled “External AI access through @hoody.com”Any AI that can fetch a URL can read @hoody.com and receive a Skill: a machine-readable map of your infrastructure’s HTTP surface. That includes ChatGPT, Claude, Claude Code, Cline, Roo Code, and Codex, with no adapter, plugin, or custom integration for any of them. The agent fetches a URL and learns your API. From that point on it can spawn containers, execute code, read files, and query databases (everything covered in this guide) from any platform that can make an HTTP request.
You are also not locked into a single AI provider. Hoody AI serves 300+ models through its gateway with no key in the container. With your own key, hoody-agent also reaches the providers in its catalogue, such as Anthropic, OpenAI and Groq, and an HTTPS endpoint that speaks the OpenAI or Anthropic API, such as a vLLM server running your own fine-tuned Llama, can be added as a custom provider. Code in the container can also call any OpenAI-compatible endpoint directly. Swapping models is a config change, and A/B testing Claude against GPT-4o is two containers. The agents you build here work with all of them. See Hoody AI for the full provider list.
What’s Next
Section titled “What’s Next”- The Vibe Coding Revolution: watch AI build your app in real time
- Multiplayer by Default: humans and agents collaborating simultaneously
- Private Workflows: keep agent experiments on hardware you rent, one disposable container each