Private Workflows
Section titled “Private Workflows”Every cloud provider makes the same promise: your data is secure. The constraint is architectural rather than a matter of intent. To serve your data, a provider’s systems must be able to read it, which leaves the data reachable by that provider, by anyone who compromises it, and by any authority that compels it.
Hoody’s architecture leaves far less to take on trust. Your data lives on a machine rented to you and is served from that machine directly, with no central Hoody proxy tier in the path. That is a property of the design, not a claim that Hoody is more trustworthy.
The servers you rent are bare metal: physical machines, not virtual instances on shared hardware or containers on someone else’s hypervisor. You control the disk, the memory, and the network. Your containers run on that machine, and your data never transits a Hoody datacenter: the proxy that serves your URLs runs on your own server, not on Hoody’s.
None of this is a privacy feature layered on top; it is how the platform is built.
Where data lives
Section titled “Where data lives”Your data does not live in a Hoody datacenter and does not pass through a Hoody proxy tier. It sits on a machine rented to you, served by software running on that same machine. Here is how that changes the trust picture:
| Layer | Traditional cloud | Hoody |
|---|---|---|
| Hardware | Shared with other tenants | Dedicated bare metal you control |
| Hypervisor | Provider-controlled | None; containers run on your hardware |
| Disk encryption | Provider holds the keys | LUKS on every host, always on; the key is never stored on the machine it unlocks, so seized hardware yields ciphertext |
| Network | Provider can inspect traffic | TLS terminates on your own rented server, with no central proxy tier in the path |
| Backups | Provider can read snapshots | Snapshots live on your disk |
| AI training | Your data may be used | Not used for training; paid model calls reach the upstream provider |
In traditional cloud, your data sits in the provider’s datacenter. In Hoody, it sits on a machine rented to you, and Hoody administers that machine rather than hosting your data. The server is a physical machine, the containers are processes on it, and the data is bytes on its disk. Hoody manages the orchestration layer (container creation, proxy routing, service coordination); the data itself stays on hardware you control.
Encrypted filesystems
Section titled “Encrypted filesystems”hoody-files supports encrypted storage through the crypt backend, which wraps another storage backend and encrypts file contents and filenames before they are written to it, then decrypts transparently on read. The passphrase lives in your container’s backend configuration, sealed in a manifest encrypted with ChaCha20-Poly1305, and is never returned by the API.
# Configure encrypted storage backend (the crypt layer). The new backend has# its own File ID. Capture it: every later call selects the backend by that id.BACKEND_ID=$(hoody files backends connect crypt -c $CONTAINER_ID \ --remote "<connected-backend-id>:secure-data" \ --password "$ENCRYPTION_KEY" -o json | jq -r '.data.id')
# Write a file to encrypted storage. The contents come from the request body:# pipe them in, or point --input at a local file.hoody files put secrets/api-keys.json -c $CONTAINER_ID \ --backend "$BACKEND_ID" \ --input ./api-keys.json
# Read it back, transparently decryptedhoody files get secrets/api-keys.json -c $CONTAINER_ID \ --backend "$BACKEND_ID"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER,});
// Configure encrypted backendconst vault = await containerClient.files.backends.connectCrypt({ remote: '<connected-backend-id>:secure-data', password: process.env.ENCRYPTION_KEY,});
// Write an encrypted file; reference the backend by its returned idconst payload = JSON.stringify({ stripe: 'sk_live_...', github: 'ghp_...' });await containerClient.files.put( 'secrets/api-keys.json', new Blob([payload]), { backend: vault.data.id },);
// Read transparently decryptedconst secrets = await containerClient.files.get('secrets/api-keys.json', { backend: vault.data.id });# Configure encrypted backend. Returns { data: { id, ... } }; use data.id for subsequent callscurl -X POST "https://$PROJECT_ID-$CONTAINER_ID-files-1.$SERVER.containers.hoody.com/api/v1/backends/crypt" \ -H "Content-Type: application/json" \ -d '{ "remote": "<connected-backend-id>:secure-data", "password": "'$ENCRYPTION_KEY'" }'
# Write to encrypted storage. The request body is the file content; the# backend is selected via the ?backend= query parameter (replace $BACKEND_ID# with the id returned above).curl -X PUT "https://$PROJECT_ID-$CONTAINER_ID-files-1.$SERVER.containers.hoody.com/api/v1/files/secrets/api-keys.json?backend=$BACKEND_ID" \ -H "Content-Type: application/json" \ -d '{"stripe": "sk_live_...", "github": "ghp_..."}'
# Read from encrypted storage (transparently decrypted)curl "https://$PROJECT_ID-$CONTAINER_ID-files-1.$SERVER.containers.hoody.com/api/v1/files/secrets/api-keys.json?backend=$BACKEND_ID"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
Configures the crypt-wrapped backend, then writes and reads a file through it. Capture BACKEND_ID from the first response before running the write and read links.
# Configure encrypted backend
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/backends/crypt&method=POST&json={"remote":"<connected-backend-id>:secure-data","password":"ENCRYPTION_KEY"}&response=transparent
# Write encrypted file
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/secrets/api-keys.json?backend=BACKEND_ID&method=PUT&json={"stripe":"sk_live_...","github":"ghp_..."}&response=transparent
# Read encrypted file
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-files-1.SERVER.containers.hoody.com/api/v1/files/secrets/api-keys.json?backend=BACKEND_ID&method=GET&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.
On disk, secrets/api-keys.json is stored as encrypted bytes. Extracting the disk or reading the underlying storage out-of-band yields ciphertext; the file is readable only through the hoody-files service with the correct key. Anyone who can reach the running service reads plaintext, because the service holds the key, so proxy permissions remain the control that matters for live access.
Secrets in the KV store
Section titled “Secrets in the KV store”For secrets that need to be available to applications without storing them in plaintext files, use hoody-sqlite’s KV store as a secrets vault:
# Store secrets in the KV store (--db is required; --create-db-if-missing on first use)hoody kv set "vault:stripe_key" --db /hoody/databases/app.db --create-db-if-missing \ --body '"sk_live_abc123..."'
hoody kv set "vault:database_url" --db /hoody/databases/app.db \ --body '"postgres://user:pass@host:5432/db"'
hoody kv set "vault:jwt_secret" --db /hoody/databases/app.db \ --body '"your-256-bit-secret"'
# Retrieve secrets programmaticallyhoody kv get "vault:stripe_key" --db /hoody/databases/app.dbimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
const containerClient = await client.withContainer({ id: CONTAINER_ID, project_id: PROJECT_ID, server: SERVER,});
// Store secrets. `key`, `db`, and `data` are required.await containerClient.sqlite.kvStore.set('vault:stripe_key', 'sk_live_abc123...', { db: '/hoody/databases/app.db', create_db_if_missing: true });
await containerClient.sqlite.kvStore.set('vault:jwt_secret', 'your-256-bit-secret', { db: '/hoody/databases/app.db' });
// Retrieve in your applicationconst stripeKey = await containerClient.sqlite.kvStore.get('vault:stripe_key', { db: '/hoody/databases/app.db' });# Store a secret. The body is the value (raw string), and `db` is required.curl -X PUT "https://$PROJECT_ID-$CONTAINER_ID-sqlite-1.$SERVER.containers.hoody.com/api/v1/sqlite/kv/vault:stripe_key?db=/hoody/databases/app.db&create_db_if_missing=true" \ -H "Content-Type: application/octet-stream" \ -d 'sk_live_abc123...'
# Retrieve a secret (the response body is the raw stored value)curl "https://$PROJECT_ID-$CONTAINER_ID-sqlite-1.$SERVER.containers.hoody.com/api/v1/sqlite/kv/vault:stripe_key?db=/hoody/databases/app.db"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
Writes a secret into the KV store, creating the database file if it doesn’t exist yet, then reads the raw value back.
# Store secret
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-sqlite-1.SERVER.containers.hoody.com/api/v1/sqlite/kv/vault:stripe_key?db=/hoody/databases/app.db%26create_db_if_missing=true&method=PUT&header=Content-Type:%20application/octet-stream&data=sk_live_abc123...&response=transparent
# Retrieve secret
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-CONTAINER_ID-sqlite-1.SERVER.containers.hoody.com/api/v1/sqlite/kv/vault:stripe_key?db=/hoody/databases/app.db&method=GET&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 KV store is a SQLite database on your bare metal disk. Secrets rest on your rented machine and are served from it directly, with no central Hoody tier in the path. SQLite itself is not encrypted: the LUKS volume under it covers the machine being stolen, and mounting that database on a crypt-wrapped backend adds the layer that still holds while the host is running.
Container isolation as the security boundary
Section titled “Container isolation as the security boundary”Each container is a complete, isolated Linux environment, and the isolation is enforced at the operating system level:
┌──────────────────────────────────────────────────┐│ YOUR BARE METAL SERVER ││ ││ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ ││ │ Container A │ │ Container B │ │ Cont. C │ ││ │ │ │ │ │ │ ││ │ Own files │ │ Own files │ │ Own files│ ││ │ Own network │ │ Own network │ │ Own net │ ││ │ Own procs │ │ Own procs │ │ Own procs│ ││ │ Own users │ │ Own users │ │ Own users│ ││ │ │ │ │ │ │ ││ │ CANNOT SEE │ │ CANNOT SEE │ │ CANNOT │ ││ │ B or C │ │ A or C │ │ SEE A, B │ ││ └─────────────┘ └─────────────┘ └──────────┘ ││ ││ Shared: CPU, RAM, Disk (but isolated views) │└──────────────────────────────────────────────────┘What that isolation means in practice:
- Container A cannot read Container B’s files, even though they share the same physical disk
- A process in Container A cannot see or kill processes in Container B
- Network traffic is isolated: containers cannot sniff each other’s traffic
- A compromised container is contained to itself, and nothing it can reach gives it your other containers
- An AI agent running in Container A has full root inside A and exactly the network reach you leave it: egress starts open, and the firewall pattern below closes it
When you give an AI agent root access to a container, what it can touch is that one container, not your server or your other projects. No isolation boundary is absolute; the point is that the unit of compromise is small, disposable, and restorable from a snapshot in seconds.
Privacy patterns
Section titled “Privacy patterns”Realm-restricted tokens for tenant isolation
Section titled “Realm-restricted tokens for tenant isolation”When building multi-tenant applications, use realms to create isolated API scopes:
# NOTE: Realms are not created. A realm is any 24-hex identifier you attach to# resources via realm_ids; it starts existing the moment a resource carries it.# `hoody realms list` reports the realm IDs already in use across your resources:hoody realms list
# Create containers within each realm by assigning the label at creation timehoody containers create --project $PROJECT_ID \ --server-id $SERVER_ID \ --name "acme-app" \ --realm-ids 507f1f77bcf86cd799439011 \ --hoody-kit
hoody containers create --project $PROJECT_ID \ --server-id $SERVER_ID \ --name "globex-app" \ --realm-ids 507f1f77bcf86cd799439012 \ --hoody-kitimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// NOTE: There is no createRealm method in the SDK, because realms are not// created; a realm is any 24-hex identifier you attach to resources via// realm_ids. Pick one per tenant and assign it at creation time. Realms are// returned as an opaque array of 24-hex IDs; any human naming convention is// external/out-of-band (e.g. tracked in your own notes).const acmeRealmId = '507f1f77bcf86cd799439011';const globexRealmId = '507f1f77bcf86cd799439012';
// list() discovers the realm IDs already present on your resourcesconst realms = await client.api.realms.list();console.log('Realm IDs in use:', realms.data.realm_ids); // string[] of 24-hex IDs
// Containers in different realms are completely isolatedconst acmeApp = await client.api.containers.create(PROJECT_ID, { name: 'acme-app', server_id: SERVER_ID, realm_ids: [acmeRealmId], hoody_kit: true,});
const globexApp = await client.api.containers.create(PROJECT_ID, { name: 'globex-app', server_id: SERVER_ID, realm_ids: [globexRealmId], hoody_kit: true,});# NOTE: There is no POST /api/v1/realms endpoint; realms are not created.# A realm is any 24-hex identifier you attach to resources via realm_ids.# GET /api/v1/realms/ reports the realm IDs already in use across your resources:curl "https://api.hoody.com/api/v1/realms/" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Create container in realmcurl -X POST "https://api.hoody.com/api/v1/projects/$PROJECT_ID/containers" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "acme-app", "server_id": "'$SERVER_ID'", "realm_ids": ["'$ACME_REALM_ID'"], "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
Lists the realm IDs already attached to your resources, then creates a container tagged into a realm via realm_ids — a realm exists only as that label, never as a resource of its own.
# List realm IDs in use
https://PROJECT_ID-CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/realms/&method=GET&bearer_token=TOKEN&response=transparent
# Create container in realm
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":"acme-app","server_id":"SERVER_ID","realm_ids":["ACME_REALM_ID"],"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.
Realm-scoped API tokens can only access containers within their realm. A token restricted to the acme realm cannot see, list, or access any container in the globex realm. The isolation is enforced at the API level.
Encryption at rest
Section titled “Encryption at rest”Layer encryption for sensitive data:
// Layer 1: hoody-files crypt backend encrypts the filesystem// Layer 2: Application-level encryption for specific fields
// @mode serverlessconst SQLITE = "https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.com";
// Store with application-level encryptionconst crypto = require('crypto');const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');const iv = crypto.randomBytes(16);const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
const sensitiveData = JSON.stringify({ ssn: '123-45-6789', salary: 150000 });let encrypted = cipher.update(sensitiveData, 'utf8', 'hex');encrypted += cipher.final('hex');
await fetch(SQLITE + "/api/v1/sqlite/db?db=/hoody/databases/app.db", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ transaction: [{ query: "INSERT INTO employee_records (name, encrypted_data, iv) VALUES (?, ?, ?)", values: ["Alice Chen", encrypted, iv.toString('hex')] }] })});Two layers: the crypt backend encrypts the filesystem, and the application encrypts sensitive fields again. Even if someone bypasses the filesystem encryption, the data inside is still ciphertext.
Egress control with the firewall
Section titled “Egress control with the firewall”Prevent containers from sending data to unauthorized destinations:
# Start from a clean firewall (reset detaches the ACL and returns the# container to an open network state), then build an allow-list and a# final deny rule so only approved destinations can be reached.# `reset` is destructive, so it asks for confirmation; pass -y to run unattended.hoody firewall reset -c $CONTAINER_ID -y
# Resolve the hostnames you want to allow to IPv4/CIDR first; the firewall# API only accepts numeric destinations (host `dig +short api.stripe.com`).STRIPE_CIDR=$(dig +short api.stripe.com | awk '{print $1"/32"; exit}')GITHUB_CIDR=$(dig +short api.github.com | awk '{print $1"/32"; exit}')
hoody firewall egress create -c $CONTAINER_ID \ --action allow --protocol tcp --destination-port 443 \ --destination "$STRIPE_CIDR" --description "Allow Stripe API"
hoody firewall egress create -c $CONTAINER_ID \ --action allow --protocol tcp --destination-port 443 \ --destination "$GITHUB_CIDR" --description "Allow GitHub API"
# Finally, drop everything else outbound so only the approved destinations# above remain reachable. Add one rule per protocol to cover tcp, udp, and icmp4.# tcp and udp rules must carry a destination port; 1-65535 covers every one of them.hoody firewall egress create -c $CONTAINER_ID \ --action drop --protocol tcp --destination "0.0.0.0/0" --destination-port 1-65535 \ --description "Deny all TCP egress"hoody firewall egress create -c $CONTAINER_ID \ --action drop --protocol udp --destination "0.0.0.0/0" --destination-port 1-65535 \ --description "Deny all UDP egress"hoody firewall egress create -c $CONTAINER_ID \ --action drop --protocol icmp4 --destination "0.0.0.0/0" --description "Deny all ICMP egress"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// Reset detaches the firewall and returns the container to an open network// state: the clean baseline to build an allow-list on top of.await client.api.firewall.reset(CONTAINER_ID);
// Whitelist specific destinations. The egress `destination` must be an// IPv4 address or CIDR range; resolve the hostname to an IP first.await client.api.firewall.addEgressRule(CONTAINER_ID, { destination: '203.0.113.10/32', // resolved IP for api.stripe.com destination_port: '443', action: 'allow', protocol: 'tcp', description: 'Allow Stripe API',});
// Drop all other outbound traffic (one rule per protocol) so only approved IPs remain.// tcp and udp rules must carry a destination_port; '1-65535' covers every port.for (const protocol of ['tcp', 'udp'] as const) { await client.api.firewall.addEgressRule(CONTAINER_ID, { destination: '0.0.0.0/0', destination_port: '1-65535', action: 'drop', protocol, description: `Deny all ${protocol} egress`, });}await client.api.firewall.addEgressRule(CONTAINER_ID, { destination: '0.0.0.0/0', action: 'drop', protocol: 'icmp4', description: 'Deny all icmp4 egress',});# Reset firewall (detaches the ACL, returns container to an open state)curl -X POST "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/firewall/reset" \ -H "Authorization: Bearer $HOODY_TOKEN"
# Whitelist specific destination. `destination` must be an IPv4 address or# CIDR range; resolve the hostname to an IP first (e.g. dig +short api.stripe.com).curl -X POST "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/firewall/egress" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "destination": "203.0.113.10/32", "destination_port": "443", "action": "allow", "protocol": "tcp", "description": "Allow Stripe API" }'
# Drop all other outbound traffic (one rule per protocol) so only approved IPs remain reachable.# tcp and udp rules must carry a destination_port; "1-65535" covers every port.curl -X POST "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/firewall/egress" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"destination": "0.0.0.0/0", "destination_port": "1-65535", "action": "drop", "protocol": "tcp", "description": "Deny all TCP egress"}'curl -X POST "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/firewall/egress" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"destination": "0.0.0.0/0", "destination_port": "1-65535", "action": "drop", "protocol": "udp", "description": "Deny all UDP egress"}'curl -X POST "https://api.hoody.com/api/v1/containers/$CONTAINER_ID/firewall/egress" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"destination": "0.0.0.0/0", "action": "drop", "protocol": "icmp4", "description": "Deny all ICMP egress"}'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
Each rule as a single GET link, run in order from a different running container’s curl-1 (OTHER_CONTAINER_ID below): once the deny-all rules land on CONTAINER_ID, that container’s own outbound egress is cut off, including the request applying the next rule.
# Reset firewall
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/firewall/reset&method=POST&bearer_token=TOKEN&response=transparent
# Allow Stripe API
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/firewall/egress&method=POST&bearer_token=TOKEN&json={"destination":"203.0.113.10/32","destination_port":"443","action":"allow","protocol":"tcp","description":"Allow%20Stripe%20API"}&response=transparent
# Deny all TCP egress
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/firewall/egress&method=POST&bearer_token=TOKEN&json={"destination":"0.0.0.0/0","destination_port":"1-65535","action":"drop","protocol":"tcp","description":"Deny%20all%20TCP%20egress"}&response=transparent
# Deny all UDP egress
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/firewall/egress&method=POST&bearer_token=TOKEN&json={"destination":"0.0.0.0/0","destination_port":"1-65535","action":"drop","protocol":"udp","description":"Deny%20all%20UDP%20egress"}&response=transparent
# Deny all ICMP egress
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/CONTAINER_ID/firewall/egress&method=POST&bearer_token=TOKEN&json={"destination":"0.0.0.0/0","action":"drop","protocol":"icmp4","description":"Deny%20all%20ICMP%20egress"}&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.
With an explicit allow-list followed by a deny-all egress rule, the container cannot phone home, exfiltrate data, or reach any server you have not explicitly approved. That is essential when running untrusted code or AI agents that might attempt to send data externally.
Compliance and data sovereignty
Section titled “Compliance and data sovereignty”Physical servers have physical locations, which keeps compliance questions concrete:
Data residency: A server in Frankfurt holds your data on a disk in Frankfurt. It is not “primarily” there, and it is not “replicated from” there.
GDPR: European personal data lives on European hardware, and container data stays on that machine unless you send it elsewhere. There is no “our servers might be anywhere” ambiguity.
HIPAA: Protected health information sits on dedicated hardware with encrypted filesystems, firewall-controlled network access, and container isolation between patient datasets.
SOC 2: The audit trail comes from HTTP request logs, access control from proxy permissions, encryption from the crypt backend, and isolation from containers. Every compliance requirement maps to an HTTP-observable, configurable control.
┌────────────────────────────────────────────────┐│ YOUR SERVER: Frankfurt, Germany ││ Physical address: DataCenter GmbH, Room 4B ││ ││ Container: patient-records ││ ├── Encrypted filesystem (crypt backend) ││ ├── Firewall: egress deny-all ││ ├── Proxy: IP whitelist (clinic IPs only) ││ ├── Realm: healthcare-prod ││ └── Snapshots: daily, 90-day retention ││ ││ Data location: This building. This rack. ││ Data access: Clinic IPs only. ││ Data encryption: crypt backend, keys on-server.││ Data retention: 90-day snapshot history. ││ Audit trail: Every HTTP request logged. │└────────────────────────────────────────────────┘A shared cloud VM cannot state that posture: the hardware is shared and the hypervisor belongs to the provider.
AI agents in locked-down containers
Section titled “AI agents in locked-down containers”An AI agent with access to a container (terminal, files, database, browser) has the same access as a developer: it can read credentials, query databases, and browse the filesystem. That reach is what makes isolation worth setting up before the agent runs.
On shared infrastructure, a compromised or misbehaving AI agent could:
- Exfiltrate data through network requests
- Access other tenants’ resources through hypervisor vulnerabilities
- Persist malicious code that survives container restarts
- Send your data to the AI provider’s training pipeline
On Hoody’s bare metal + container architecture:
- Container isolation keeps the agent inside its own container
- Firewall rules prevent unauthorized network communication
- Snapshots let you roll back any changes the agent made
- Bare metal means no hypervisor attack surface
- Full-disk encryption is always on, on every host, so a drive read outside the running machine yields ciphertext
- Opt-in encrypted storage (crypt backend) adds the layer that survives a live host: those files stay ciphertext even to something reading the unlocked volume
# The AI safety workflow:
# 1. Create an isolated container for the experiment and capture its IDEXPERIMENT_ID=$(hoody containers create --project $PROJECT_ID \ --server-id $SERVER_ID \ --name "ai-experiment" \ --hoody-kit -o json | jq -r '.id')
# 2. Lock down the network: reset to a clean baseline, then add a deny-all# egress rule so the agent has no outbound path (allow-list specific# destinations with `hoody firewall egress create` if it needs any).# `reset` is destructive, so pass -y to skip its confirmation prompt.# tcp and udp rules must carry a destination port; 1-65535 covers every one.hoody firewall reset -c $EXPERIMENT_ID -yhoody firewall egress create -c $EXPERIMENT_ID \ --action drop --protocol tcp --destination "0.0.0.0/0" --destination-port 1-65535 \ --description "Deny all TCP"hoody firewall egress create -c $EXPERIMENT_ID \ --action drop --protocol udp --destination "0.0.0.0/0" --destination-port 1-65535 \ --description "Deny all UDP"hoody firewall egress create -c $EXPERIMENT_ID \ --action drop --protocol icmp4 --destination "0.0.0.0/0" --description "Deny all ICMP"
# 3. Snapshot clean statehoody snapshots create --container $EXPERIMENT_ID \ --alias "clean-slate"
# 4. Let the AI agent run. hoody-agent is an in-container Kit service reached# through the Hoody Proxy, not the management Hoody API. Open a session bound to the container,# then dispatch a turn into it (auto-approve answers confirm gates so the# turn runs unattended). For a session-less one-shot, use# `hoody agent headless create-run` instead.SESSION_ID=$(hoody agent sessions create --realm global \ -c $EXPERIMENT_ID -o json | jq -r '.id')hoody agent sessions prompt-sync --id $SESSION_ID \ --policy auto_approve \ --text "Analyze this dataset and build a classification model"
# 5. Inspect results# 6. Restore clean state if needed. The restore key is the auto-generated# snap-<timestamp> name (from `hoody snapshots list`), not the alias.hoody snapshots restore -c $EXPERIMENT_ID --name "snap-20251109-143045" -yimport { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://api.hoody.com', token: process.env.HOODY_TOKEN });
// 1. Isolated containerconst experiment = await client.api.containers.create(PROJECT_ID, { name: 'ai-experiment', server_id: SERVER_ID, hoody_kit: true,});
// 2. Lock down the network: reset to a clean baseline, then deny all egress.// tcp and udp rules must carry a destination_port; '1-65535' covers every port.await client.api.firewall.reset(experiment.data.id);for (const protocol of ['tcp', 'udp'] as const) { await client.api.firewall.addEgressRule(experiment.data.id, { destination: '0.0.0.0/0', destination_port: '1-65535', action: 'drop', protocol, description: `Deny all ${protocol}`, });}await client.api.firewall.addEgressRule(experiment.data.id, { destination: '0.0.0.0/0', action: 'drop', protocol: 'icmp4', description: 'Deny all icmp4',});
// 3. Snapshot clean state. Capture the auto-generated snap-<timestamp>// name; the alias is just a label, not the restore key.const cleanSnap = await client.api.containers.createSnapshot(experiment.data.id, { alias: 'clean-slate',});
// 4. Let the AI work in isolation. hoody-agent is an in-container Kit service// reached through the Hoody Proxy, not the management Hoody API. Open a session bound to the container,// then dispatch a turn into it. The 'policy: auto_approve' option// auto-answers confirm gates so the turn runs unattended. For a// session-less one-shot, use client.agent.headless.createHeadlessRun(...).const session = await client.agent.sessions.createSession({ realm: 'global', container: experiment.data.id,});await client.agent.sessions.promptSync(session.id, { text: 'Analyze this dataset and build a classification model' }, { policy: 'auto_approve' });
// 5. Inspect results via files, terminal, sqlite// 6. Restore if neededawait client.api.containers.restoreSnapshot(experiment.data.id, cleanSnap.data.snapshot.name);# 1. Create isolated containercurl -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-experiment", "server_id": "'$SERVER_ID'", "hoody_kit": true}'
# 2. Lock down the network: reset to a clean baseline, then deny all egresscurl -X POST "https://api.hoody.com/api/v1/containers/$EXPERIMENT_ID/firewall/reset" \ -H "Authorization: Bearer $HOODY_TOKEN"
curl -X POST "https://api.hoody.com/api/v1/containers/$EXPERIMENT_ID/firewall/egress" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"destination": "0.0.0.0/0", "destination_port": "1-65535", "action": "drop", "protocol": "tcp", "description": "Deny all TCP"}'curl -X POST "https://api.hoody.com/api/v1/containers/$EXPERIMENT_ID/firewall/egress" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"destination": "0.0.0.0/0", "destination_port": "1-65535", "action": "drop", "protocol": "udp", "description": "Deny all UDP"}'curl -X POST "https://api.hoody.com/api/v1/containers/$EXPERIMENT_ID/firewall/egress" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"destination": "0.0.0.0/0", "action": "drop", "protocol": "icmp4", "description": "Deny all ICMP"}'
# 3. Snapshot clean state and capture the auto-generated snap-<timestamp># name (the alias is just a label, not the restore key)CLEAN_SNAP=$(curl -s -X POST "https://api.hoody.com/api/v1/containers/$EXPERIMENT_ID/snapshots" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"alias": "clean-slate"}' | jq -r '.data.snapshot.name')
# 4. Let AI work. hoody-agent is an in-container Kit service; reach it at the# container's own agent URL through the Hoody Proxy (NOT api.hoody.com).# Open a session on that container, then dispatch a turn into it.# X-Hoody-Gate-Policy: auto_approve (or ?policy=auto_approve) auto-answers# confirm gates. For a session-less one-shot, POST to that URL's# /api/v1/agent/headless/runs instead.SESSION_ID=$(curl -s -X POST "https://$PROJECT_ID-$EXPERIMENT_ID-agent-1.$SERVER.containers.hoody.com/api/v1/agent/sessions?realm=global" \ -H "Authorization: Bearer $HOODY_TOKEN" | jq -r '.id')
curl -X POST "https://$PROJECT_ID-$EXPERIMENT_ID-agent-1.$SERVER.containers.hoody.com/api/v1/agent/sessions/$SESSION_ID/prompt:sync" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -H "X-Hoody-Gate-Policy: auto_approve" \ -d '{"text": "Analyze this dataset and build a classification model"}'
# 5-6. Inspect and restore if needed (restore = PUT the snapshot by name)curl -X PUT "https://api.hoody.com/api/v1/containers/$EXPERIMENT_ID/snapshots/$CLEAN_SNAP" \ -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
Every step as one link each, run in order from a different running container’s curl-1 (OTHER_CONTAINER_ID below): EXPERIMENT_ID does not exist until the first link creates it, and its own egress is cut off once the deny-all rules land. The snapshot and session links each return an id in their response — CLEAN_SNAP (data.snapshot.name) and SESSION_ID (id) — that you copy into the later links before running them.
# 1. Create isolated container
https://PROJECT_ID-OTHER_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-experiment","server_id":"SERVER_ID","hoody_kit":true}&response=transparent
# 2. Reset firewall
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/EXPERIMENT_ID/firewall/reset&method=POST&bearer_token=TOKEN&response=transparent
# 2. Deny all TCP
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/EXPERIMENT_ID/firewall/egress&method=POST&bearer_token=TOKEN&json={"destination":"0.0.0.0/0","destination_port":"1-65535","action":"drop","protocol":"tcp","description":"Deny%20all%20TCP"}&response=transparent
# 2. Deny all UDP
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/EXPERIMENT_ID/firewall/egress&method=POST&bearer_token=TOKEN&json={"destination":"0.0.0.0/0","destination_port":"1-65535","action":"drop","protocol":"udp","description":"Deny%20all%20UDP"}&response=transparent
# 2. Deny all ICMP
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://api.hoody.com/api/v1/containers/EXPERIMENT_ID/firewall/egress&method=POST&bearer_token=TOKEN&json={"destination":"0.0.0.0/0","action":"drop","protocol":"icmp4","description":"Deny%20all%20ICMP"}&response=transparent
# 3. 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/EXPERIMENT_ID/snapshots&method=POST&bearer_token=TOKEN&json={"alias":"clean-slate"}&response=transparent
# 4. Open agent session
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-EXPERIMENT_ID-agent-1.SERVER.containers.hoody.com/api/v1/agent/sessions?realm=global&method=POST&bearer_token=TOKEN&response=transparent
# 4. Prompt the agent
https://PROJECT_ID-OTHER_CONTAINER_ID-curl-1.SERVER.containers.hoody.com/api/v1/curl/request?url=https://PROJECT_ID-EXPERIMENT_ID-agent-1.SERVER.containers.hoody.com/api/v1/agent/sessions/SESSION_ID/prompt:sync&method=POST&bearer_token=TOKEN&header=X-Hoody-Gate-Policy:%20auto_approve&json={"text":"Analyze%20this%20dataset%20and%20build%20a%20classification%20model"}&response=transparent
# 6. Restore clean 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/EXPERIMENT_ID/snapshots/CLEAN_SNAP&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.
The AI agent has full autonomy inside an air-gapped container. It can read data, write code, and run experiments, but it cannot exfiltrate anything because the firewall blocks all outbound traffic. When the experiment is done, inspect the results; if anything looks wrong, restore the clean snapshot. The data never left your hardware.
Defense in depth
Section titled “Defense in depth”The security posture layers eight controls:
Layer 1: Bare Metal → No shared hardware, no hypervisor attack surfaceLayer 2: Container → Process isolation, filesystem isolationLayer 3: Firewall → Network egress control, default-allow; add rules to restrictLayer 4: Proxy Permissions → Authentication before HTTP reaches the containerLayer 5: Encrypted FS → LUKS on every host by default; crypt backend adds a live-host layerLayer 6: Application → Field-level encryption, input validationLayer 7: Snapshots → Rollback capability, audit via diffLayer 8: Realms → API-level tenant isolationEach layer is independently configurable through HTTP, and each can be audited and changed the same way. The layers are not fully independent of one another (containers on a host share a kernel), but they fail separately: compromising one does not hand an attacker the rest.
What’s Next
Section titled “What’s Next”- Building a Full-Stack Application: build with security from the start
- Deploying Autonomous AI Agents: AI agents in isolated containers
- Proxy Permissions: fine-grained access control
- Firewall Configuration: network-level security
- Encrypted Cloud Storage: multi-backend encrypted storage