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 terminal-1.../api/v1/terminal/execute |
| Read/write files | SFTP + mount points + permissions | GET/POST files.../api/v1/files/... |
| Query databases | Connection strings + drivers + ORM | POST sqlite-1.../api/v1/sqlite/db |
| Automate browsers | Puppeteer setup + Chromium install | GET browser-1.../screenshot, GET browser-1.../browse |
| 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, authenticated by nothing more than a bearer token. There is no SDK to install and no driver to manage.
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 18 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 prompt-sync` / `prompt-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 '.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`;
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.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.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 '.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 it to finish. Copy the 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; without it, terminal_id is required.curl -X POST "https://$PROJECT_ID-$AGENT_ID-terminal-1.$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 any file. 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).# browser_id (the 0-based instance index) is required.curl "https://$PROJECT_ID-$AGENT_ID-browser-1.$SERVER.containers.hoody.com/screenshot?browser_id=0&url=http://localhost:3000&start=true" \ --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 waits for completion before returning.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.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();// result.status is already final ('done', 'error', 'canceled', or 'quit'), so no polling is needed
// 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).const testResult = await containerClient.terminal.execution.execute( { command: 'cd /app && bun test payments' }, { ephemeral: true },);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 per session, and 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.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.hoody agent tools list-session-mcp --id $SESSION_ID --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 snapshot's machine name (e.g. snap-20250115-103000) so the// rollback can target the exact snapshot; the alias is only a friendly label.const snapshot = await client.api.containers.createSnapshot(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 agent to finish before returning.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.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 is synchronous: result.status is already final.// Roll back on anything other than a clean 'done' (covers 'error', 'canceled', 'quit').if (result.status !== 'done') { // Rollback: restore the snapshot created in Step 1 (by machine name, not alias) await client.api.containers.restoreSnapshot(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.notify.trigger({ 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 the container back in seconds. 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:
# 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 '.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: the ID from# `list`, not the alias (`create` doesn't print it, so look it up first):SNAP_NAME=$(hoody snapshots list -c $AGENT_ID -o json \ | jq -r '.snapshots[] | select(.alias=="before-agent-experiment") | .name' | head -n1)hoody snapshots restore -c $AGENT_ID --name "$SNAP_NAME" -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.containers.createSnapshot(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`;
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.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.containers.restoreSnapshot(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 '.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 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 access another’s filesystem, processes, or network unless explicitly connected via HTTP
- Sandboxed experiments: an agent testing a destructive migration cannot touch your production data
- Contained failures: if an agent installs malicious packages or runs a fork bomb, only its container is affected
- Preserved evidence: snapshot the container after an agent run to audit exactly what it did
This is why bare metal plus containers is the architecture for AI: not because it is more convenient, but because the containment is physical, on hardware that is yours alone.
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 supports 300+ models from 15+ gateway inference providers, among them Anthropic, OpenAI, Google, Mistral, Deepseek, and xAI, plus any OpenAI-compatible endpoint you point it at (Groq, local Ollama, Azure OpenAI, your own fine-tuned Llama). 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