Understand containers
What makes a Hoody container different from a VM and a Docker image. Containers →
Sign in once, create a container, and every terminal, file, and database on it is a URL. In this walkthrough you’ll spawn a computer, run code on it, and drive it from the CLI, a TypeScript program, and plain curl. There is no deployment step and no certificate to configure; every call is an ordinary HTTPS request.
Hoody has one control plane and several ways to reach it. All of them talk to the same account and the same containers, so switching between them requires no migration.
Four programmatic interfaces (the walkthrough below covers the first three):
| Interface | Setup |
|---|---|
| CLI | npm install -g hoody-sdk, or without installing: npx hoody-sdk <command> |
| TypeScript SDK | npm install hoody-sdk@beta (Node.js 22.23+, or 24.18+ on the 24 line, or Bun) |
| Raw HTTP | curl with a Bearer token on api.hoody.com |
| Browser SDK | Pinned CDN build that exposes window.HoodySDK in any static page |
Three entry points that need no code:
| Entry point | What happens |
|---|---|
ssh hoody.com | Signs you in to a sandboxed Hoody CLI and launches the Hoody Agent TUI, with mouse support, from any machine with an ssh client; nothing is installed |
os.hoody.com | The Hoody Agent in any browser; see Step 4 |
@hoody.com | Paste it into ChatGPT, Claude, or any web-fetching AI agent; it fetches a Skill and drives your account with a token you give it |
npx skills add | Installs the Hoody Skill into a coding agent you already run, so it knows the whole HTTP surface without being told each time |
If you already work in a coding agent, install the Hoody Skill into it once and it knows the whole HTTP surface:
# The skill itself; deeper docs are fetched on demandnpx skills add https://hoody.com/SKILLS/SKILL.md
# The same skill plus the whole corpus on disk, for offline usenpx skills add HoodyNetwork/hoody-sdkWorks with
CLI runs on
The agent still needs a token you give it. The Skill teaches it the endpoints; it does not grant access.
If you don’t have an account, create one at hoody.com/signup; it comes with a free-tier server. Then authenticate with whichever interface you picked:
# Install the CLI (it ships inside the hoody-sdk package)...npm install -g hoody-sdk# ...or skip the install entirely: npx hoody-sdk <command>
# Interactive sign-in (`hoody signup` if you don't have an account yet)hoody login// npm install hoody-sdk@betaimport { HoodyClient } from 'hoody-sdk';
const hoody = await HoodyClient.authenticate('https://api.hoody.com', { username: process.env.HOODY_EMAIL!, password: process.env.HOODY_PASSWORD!,});# Print a token from your CLI session (or mint a scoped one with `hoody auth create`)export HOODY_TOKEN=$(hoody login --print-token)
# Every control-plane call carries it as a Bearer headercurl -H "Authorization: Bearer $HOODY_TOKEN" \ https://api.hoody.com/api/v1/users/auth/meOne 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
Confirms the token is valid and returns the signed-in account.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/users/auth/me&method=GET&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.
The chain is short: your account holds servers, a project organizes containers, and a container is a full Linux computer running Debian with systemd. Your free-tier server is already on the account; grab its id, then build on it.
# Your free-tier server is already there; grab its server_id# (-o json prints the response data itself, no envelope to unwrap)SERVER_ID=$(hoody servers list -o json \ | jq -r '[.[] | select(.status == "active")][0].server_id')
# Create a project, then spawn a Kit container on that serverPROJECT_ID=$(hoody projects create --alias "my-first-project" -o json | jq -r '.id')CONTAINER=$(hoody containers create --project $PROJECT_ID --server-id $SERVER_ID \ --name "dev-box" --hoody-kit -o json)CONTAINER_ID=$(echo "$CONTAINER" | jq -r '.id')SERVER_NAME=$(echo "$CONTAINER" | jq -r '.server_name')
# Wait until status is "running"; then the Kit URLs are livehoody containers get $CONTAINER_ID -o json | jq -r '.status'// Your free-tier server is already there; grab its server_idconst rentals = (await hoody.api.serverRental.list()).data ?? [];const serverId = rentals.find(r => r.status === 'active' && r.server_id)!.server_id!;
// Create a project, then spawn a Kit container on that serverconst project = await hoody.api.projects.create({ alias: 'my-first-project' });const { data: container } = await hoody.api.containers.create(project.data!.id, { server_id: serverId, name: 'dev-box', hoody_kit: true, // preinstall the Kit service layer: terminal, files, sqlite, agent, ...});
// Poll until "running", then scope a client to the boxwhile ((await hoody.api.containers.get(container!.id)).data!.status !== 'running') { await new Promise(r => setTimeout(r, 1000));}const box = await hoody.withContainer(container!);# Your free-tier server is already there; grab its server_idSERVER_ID=$(curl -s -H "Authorization: Bearer $HOODY_TOKEN" \ https://api.hoody.com/api/v1/servers \ | jq -r '[.data[] | select(.status == "active")][0].server_id')
# Create a projectPROJECT_ID=$(curl -s -X POST https://api.hoody.com/api/v1/projects/ \ -H "Authorization: Bearer $HOODY_TOKEN" -H "Content-Type: application/json" \ -d '{"alias": "my-first-project"}' | jq -r '.data.id')
# Spawn a Kit container on that serverCONTAINER=$(curl -s -X POST https://api.hoody.com/api/v1/projects/$PROJECT_ID/containers \ -H "Authorization: Bearer $HOODY_TOKEN" -H "Content-Type: application/json" \ -d '{"server_id": "'"$SERVER_ID"'", "name": "dev-box", "hoody_kit": true}')CONTAINER_ID=$(echo "$CONTAINER" | jq -r '.data.id')SERVER_NAME=$(echo "$CONTAINER" | jq -r '.data.server_name')
# Wait until status is "running"; then the Kit URLs are livecurl -s -H "Authorization: Bearer $HOODY_TOKEN" \ https://api.hoody.com/api/v1/containers/$CONTAINER_ID | jq -r '.data.status'Every Kit service on your container answers at a stable HTTPS URL from the moment it exists:
https://{projectId}-{containerId}-{service}-{index}.{serverName}.containers.hoody.comNineteen services share that pattern: terminal, files, browser, display, code, exec, daemon, cron, watch, sqlite, curl, egress, pipe, run, notes, notifications, tunnel, proxyLogs, and a built-in AI agent. Two of them use short URL slugs (notifications → n, proxyLogs → logs), and http-{port} covers anything you start on a port. This step uses three of them.
# Execute a shell command on your containerhoody terminal sessions exec -c $CONTAINER_ID --ephemeral \ --command "echo 'Hello from the cloud!'"// One-shot helper: runs, waits, returns the outputconst result = await box.terminal.execution.execute({ command: "echo 'Hello from the cloud!'", wait: true });console.log(result.data.stdout); // → Hello from the cloud!# Start the command...COMMAND_ID=$(curl -s -X POST \ "https://$PROJECT_ID-$CONTAINER_ID-terminal-1.$SERVER_NAME.containers.hoody.com/api/v1/terminal/execute?ephemeral=true" \ -H "Content-Type: application/json" \ -d '{"command": "echo Hello from the cloud!", "wait": true}' | jq -r '.command_id')
# ...and fetch its outputcurl -s "https://$PROJECT_ID-$CONTAINER_ID-terminal-1.$SERVER_NAME.containers.hoody.com/api/v1/terminal/result/$COMMAND_ID" \ | jq -r '.stdout'# Read a file from your container (-o raw prints it verbatim)hoody files get -c $CONTAINER_ID /etc/hostname -o rawconst file = await box.files.get('/etc/hostname', { responseType: 'text' });console.log(file.data);curl "https://$PROJECT_ID-$CONTAINER_ID-files-1.$SERVER_NAME.containers.hoody.com/api/v1/files/etc/hostname"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
Returns the file’s raw bytes. No token needed: the project/container id pair in the URL is the credential.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER_NAME.containers.hoody.com/api/v1/files/etc/hostname&method=GET&response=transparent Every Kit container ships a SQLite service; there is no database server to provision and no connection string to configure.
# Run a SQL transaction on the built-in SQLitehoody db exec-transaction -c $CONTAINER_ID --db app --create-db-if-missing \ --transaction '[{"statement":"CREATE TABLE IF NOT EXISTS greetings (message TEXT)"},{"statement":"INSERT INTO greetings VALUES ('"'"'Hello, Hoody!'"'"')"},{"query":"SELECT * FROM greetings"}]' \ -o jsonconst result = await box.sqlite.database.executeTransaction( { transaction: [ { statement: "CREATE TABLE IF NOT EXISTS greetings (message TEXT)" }, { statement: "INSERT INTO greetings VALUES ('Hello, Hoody!')" }, { query: "SELECT * FROM greetings" }, ], }, { db: 'app', create_db_if_missing: true },);console.log(result.data);// → results: [..., { success: true, resultHeaders: ['message'], resultSet: [{ message: 'Hello, Hoody!' }] }]curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-sqlite-1.$SERVER_NAME.containers.hoody.com/api/v1/sqlite/db?db=app&create_db_if_missing=true" \ -H "Content-Type: application/json" \ -d '{"transaction": [{"statement": "CREATE TABLE IF NOT EXISTS greetings (message TEXT)"}, {"statement": "INSERT INTO greetings VALUES ('"'"'Hello, Hoody!'"'"')"}, {"query": "SELECT * FROM greetings"}]}'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 table if missing, inserts a row, and returns the select in one transaction. No token needed: the project/container id pair in the URL is the credential.
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-sqlite-1.SERVER_NAME.containers.hoody.com/api/v1/sqlite/db?db=app%26create_db_if_missing=true&method=POST&json={"transaction":[{"statement":"CREATE%20TABLE%20IF%20NOT%20EXISTS%20greetings%20(message%20TEXT)"},{"statement":"INSERT%20INTO%20greetings%20VALUES%20('Hello,%20Hoody!')"},{"query":"SELECT%20*%20FROM%20greetings"}]}&response=transparent So far you’ve driven your container through the API. Hoody also has a browser interface: go to os.hoody.com, sign in, and you land in the Hoody Agent. Chat with it, run terminals, browse files, and manage every container from one screen.
The Agent is not hosted by Hoody. Your own container serves it, so every Kit container you spin up carries its own agent, reachable directly at:
https://{projectId}-{containerId}-agent-1.{serverName}.containers.hoody.comOpen it in any browser, embed it in an iframe, or share it with a teammate. A phone, a laptop, a TV, and a tablet all reach the same environment and the same state. The agent that manages your containers is itself running in a container.
Three more ways into the same account:
ssh hoody.com: the Hoody Agent, rendered as a TUI. The gateway drops you into a memory-only sandboxed Hoody CLI session (nothing persists between connections) that launches hoody agent, with mouse support, on any machine with an ssh client. Nothing is installed on the device; sign in inside the CLI, or pass a scoped token as the SSH username (ssh <hdy_token>@hoody.com) for scripts and CI.@hoody.com: paste it into ChatGPT, Claude, Gemini, or any web-fetching AI agent. The agent fetches a Skill, a structured HTTP map of every capability on this page, and drives your account with a token you give it. There is no server to host and no plugin to install.https://cdn.jsdelivr.net/npm/hoody-sdk@1.0.0-beta.9/dist/hoody-sdk.browser.min.js) exposes window.HoodySDK in any static page. Hand pages short-lived scoped tokens, never your account credentials.In this walkthrough you:
curl, with no SSH keys and no client software.servers list returns nothing active: a fresh free-tier server may still be provisioning, and provisioning time varies. Keep polling; it appears with status: "active" and a non-null server_id.running yet (poll it), the container was created with --no-hoody-kit / hoody_kit: false, or your URL uses server_id where the hostname needs server_name.api.hoody.com: control-plane calls always need the Bearer token; re-run hoody login --print-token and re-export HOODY_TOKEN.terminal, files, db, …) target a container via -c $CONTAINER_ID; pass it explicitly or export it once as HOODY_CONTAINER_ID.projectId-containerId pair is the grant. That’s the open-by-default posture; add permission rules when you need auth groups, IP pins, or default-deny.server_id and server_name? server_id identifies the server in API calls (creating containers, rentals); server_name is the DNS segment in every container URL. Both come back in the container object.http-{port} routes are cataloged in The Hoody Kit.Understand containers
What makes a Hoody container different from a VM and a Docker image. Containers →
Route and lock down URLs
How the proxy turns every service into a URL and how permission rules gate it. Proxy →
Build your first API
Drop a script in a container and get an authenticated HTTPS endpoint. Your First API →
Explore the Kit
All 18 HTTP services built into every Kit container, including the AI agent. The Hoody Kit →