Skip to content
Hoody.com

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.

Screenshot Coming Soon AI agent session: chat window with AI writing code, terminal showing live execution, and file browser reflecting changes in real time
An agent session: chat, terminal, and file browser updating live

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.


Consider what an autonomous agent needs:

CapabilityTraditional stackHoody
Execute commandsSSH + credentials + firewall rulesPOST /api/v1/terminal/execute on terminal-1
Read/write filesSFTP + mount points + permissionsGET/PUT /api/v1/files/... on files-1
Query databasesConnection strings + drivers + ORMPOST /api/v1/sqlite/db on sqlite-1
Automate browsersPuppeteer setup + Chromium installGET /screenshot, GET /browse on browser-1
Operate GUI applicationsVNC server + desktop automation scriptsGET /api/v1/display/screenshot, POST /api/v1/display/input/act on display-1
Spawn more agentsInfrastructure provisioningPOST /api/v1/projects/{id}/containers
Observe everythingLogging infrastructureEvery 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.


Give your AI agent its own computer:

Terminal window
# Create a container for your agent
hoody containers create --project $PROJECT_ID \
--server-id $SERVER_ID \
--name "ai-agent-alpha" \
--container-image "debian/13" \
--hoody-kit

That container is a complete computer. The agent has all 19 HTTP services at its disposal without further setup.


The hoody-agent service is a full autonomous coding agent accessible entirely through HTTP, across 100+ endpoints:

Terminal window
# 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."
}'

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.

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 sessions
https://PROJECT_ID-AGENT_ID-agent-1.SERVER.containers.hoody.com/api/v1/agent/sessions
# Live event stream for one session
https://PROJECT_ID-AGENT_ID-agent-1.SERVER.containers.hoody.com/api/v1/agent/sessions/SESSION_ID/stream

You 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.com

An agent in a Hoody container has access to everything a human developer would:

Terminal window
# 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"}'
Terminal window
# 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 body
curl -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"}'
Terminal window
# Agent can query and modify databases; the `db` query parameter is required
curl -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)"}]}'
Terminal window
# 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.png

Because containers are peers connected by HTTP, agents can orchestrate other 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 B
const 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 services
const 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-generate
const 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.

┌─────────────────────┐
│ 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.


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.

Terminal window
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:

Terminal window
# 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 json

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.


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.


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.

Terminal window
# Always snapshot before letting an agent run
hoody 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" -y

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.


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.