# Private Workflows

**Page:** guides/private-workflows

[Download Raw Markdown](./guides/private-workflows.md)

---

# Private Workflows

**Every cloud provider makes the same promise: "Your data is secure." Then they read your data to train AI models, hand it to governments without telling you, or suffer breaches that expose millions of records.** The promise is structurally impossible to keep because their architecture requires them to have access to your data in order to serve it.

Hoody's architecture is different. Not because we are more trustworthy -- because the 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.

You rent bare metal servers. Physical machines. Not virtual instances on shared hardware. Not containers on someone else's hypervisor. Actual hardware where 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 ours.

This is not a privacy feature. It is the architecture.

---

## What That Means in Hoody

In Hoody, this means 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 -- 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 |

The fundamental difference: 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. Your server is a physical machine. Your containers are processes on that machine. Your data is bytes on that disk. Hoody manages the orchestration layer -- container creation, proxy routing, service coordination -- but your actual data stays on hardware you control.


This is not marketing language. This is an architectural fact. Hoody's API manages container lifecycle and proxy routing. The container data -- files, databases, processes, memory -- exists on your bare metal server. Your container data lives on your own bare metal rather than in a Hoody datacenter. Hoody operates that machine on your behalf, so platform administration retains host-level access -- the guarantee here is about where your data lives and what it traverses, not about an operator who is technically unable to look.


---

## 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 -- decrypting 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.


  
    ```bash
    # 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 decrypted
    hoody files get secrets/api-keys.json -c $CONTAINER_ID \
      --backend "$BACKEND_ID"
    ```
  
  
    ```typescript
    import { HoodyClient } from 'hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    const containerClient = await client.withContainer({
      id: CONTAINER_ID,
      project_id: PROJECT_ID,
      server: SERVER,
    });

    // Configure encrypted backend
    const vault = await containerClient.files.backends.connectCrypt({
      remote: '<connected-backend-id>:secure-data',
      password: process.env.ENCRYPTION_KEY,
    });

    // Write encrypted file -- reference the backend by its returned id
    const 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 decrypted
    const secrets = await containerClient.files.get('secrets/api-keys.json', { backend: vault.data.id });
    ```
  
  
    ```bash
    # Configure encrypted backend -- returns { data: { id, ... } }; use data.id for subsequent calls
    curl -X POST "https://$PROJECT_ID-$CONTAINER_ID-files-1.$SERVER.containers.hoody.icu/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.icu/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.icu/api/v1/files/secrets/api-keys.json?backend=$BACKEND_ID"
    ```
  


**What happens on disk:** The file `secrets/api-keys.json` is stored as encrypted bytes. If someone physically extracts the disk, they get ciphertext. If someone reads the underlying storage out-of-band, they get ciphertext. But anyone who can reach the running hoody-files service reads plaintext -- the service holds the key -- so proxy permissions remain the control that matters for live access. The file is only readable through the hoody-files service with the correct key.

---

## Vault Secrets Management

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:


  
    ```bash
    # 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 programmatically
    hoody kv get "vault:stripe_key" --db /hoody/databases/app.db
    ```
  
  
    ```typescript
    import { HoodyClient } from 'hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', 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 application
    const stripeKey = await containerClient.sqlite.kvStore.get('vault:stripe_key', { db: '/hoody/databases/app.db' });
    ```
  
  
    ```bash
    # 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.icu/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.icu/api/v1/sqlite/kv/vault:stripe_key?db=/hoody/databases/app.db"
    ```
  


The KV store lives in SQLite, which lives on your bare metal disk. Secrets rest on your rented machine and are served from it directly -- 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

Each container is a complete, isolated Linux environment. The isolation is not just logical -- it 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 container 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

This is why container isolation is the right security primitive for the AI era. When you give an AI agent root access to a container, what it can touch is that one container -- not your server, not 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

### Pattern 1: Realm-Restricted Tokens for Tenant Isolation

When building multi-tenant applications, use realms to create isolated API scopes:


  
    ```bash
    # 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 time
    hoody 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-kit
    ```
  
  
    ```typescript
    import { HoodyClient } from 'hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', 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 resources
    const 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 isolated
    const 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,
    });
    ```
  
  
    ```bash
    # 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.icu/api/v1/realms/" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    # Create container in realm
    curl -X POST "https://api.hoody.icu/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
      }'
    ```
  


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.

### Pattern 2: Encrypt Data at Rest

Layer encryption for sensitive data:

```typescript
// Layer 1: hoody-files crypt backend encrypts the filesystem
// Layer 2: Application-level encryption for specific fields

// @mode serverless
const SQLITE = "https://PROJECT-CONTAINER-sqlite-1.SERVER.containers.hoody.icu";

// Store with application-level encryption
const 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 filesystem is encrypted by the crypt backend, and sensitive fields are encrypted again at the application level. Even if someone bypasses the filesystem encryption, the data inside is still ciphertext.

### Pattern 3: Control Network Egress with Firewall

Prevent containers from sending data to unauthorized destinations:


  
    ```bash
    # 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"
    ```
  
  
    ```typescript
    import { HoodyClient } from 'hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', 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',
    });
    ```
  
  
    ```bash
    # Reset firewall (detaches the ACL, returns container to an open state)
    curl -X POST "https://api.hoody.icu/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.icu/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.icu/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.icu/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.icu/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"}'
    ```
  


With an explicit allow-list followed by a deny-all egress rule, the container cannot phone home, cannot exfiltrate data, cannot communicate with any server you have not explicitly approved. This is essential when running untrusted code or AI agents that might attempt to send data externally.

---

## Compliance and Data Sovereignty

Physical servers mean physical locations. This is the simplest compliance argument there is:

**Data residency:** Your server is in Frankfurt. Your data is in Frankfurt. Not "primarily" in Frankfurt. Not "replicated from" Frankfurt. Physically, magnetically, on a disk in Frankfurt.

**GDPR:** European personal data lives on European hardware. Your container data stays on that machine unless you send it elsewhere -- no "our servers might be anywhere" hedging.

**HIPAA:** Protected health information on dedicated hardware with encrypted filesystems, firewall-controlled network access, and container isolation between patient datasets.

**SOC 2:** Audit trail through HTTP request logs. Access control through proxy permissions. Encryption through crypt backend. Isolation through 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.        │
└────────────────────────────────────────────────┘
```

Try expressing that compliance posture with a shared cloud VM. You cannot. The architecture prevents it.

---

## The AI Safety Angle

Here is the argument that makes private workflows urgent rather than merely prudent:

**AI agents running in your infrastructure can see everything.** When you give an AI agent access to a container -- terminal, files, database, browser -- it has the same access as a developer. It can read credentials, query databases, browse the filesystem.

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


  
    ```bash
    # The AI safety workflow:

    # 1. Create an isolated container for the experiment and capture its ID
    EXPERIMENT_ID=$(hoody containers create --project $PROJECT_ID \
      --server-id $SERVER_ID \
      --name "ai-experiment" \
      --hoody-kit -o json | jq -r '.id')

    # 2. Lock down 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 -y
    hoody 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 state
    hoody 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" -y
    ```
  
  
    ```typescript
    import { HoodyClient } from 'hoody-sdk';

    const client = new HoodyClient({ baseURL: 'https://api.hoody.icu', token: process.env.HOODY_TOKEN });

    // 1. Isolated container
    const experiment = await client.api.containers.create(PROJECT_ID, {
      name: 'ai-experiment',
      server_id: SERVER_ID,
      hoody_kit: true,
    });

    // 2. Lock down 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 needed
    await client.api.containers.restoreSnapshot(experiment.data.id, cleanSnap.data.snapshot.name);
    ```
  
  
    ```bash
    # 1. Create isolated container
    curl -X POST "https://api.hoody.icu/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 network — reset to a clean baseline, then deny all egress
    curl -X POST "https://api.hoody.icu/api/v1/containers/$EXPERIMENT_ID/firewall/reset" \
      -H "Authorization: Bearer $HOODY_TOKEN"

    curl -X POST "https://api.hoody.icu/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.icu/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.icu/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 — 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.icu/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.icu).
    #    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.icu/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.icu/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.icu/api/v1/containers/$EXPERIMENT_ID/snapshots/$CLEAN_SNAP" \
      -H "Authorization: Bearer $HOODY_TOKEN"
    ```
  


The AI agent has full autonomy inside an air-gapped container. It can read data, write code, 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.


This is not about preventing AI from being useful. It is about letting AI be maximally useful -- full root access, full filesystem access, full database access -- while keeping any mistake or misbehaviour to one container, on a machine you rent rather than one you share.


---

## Defense in Depth

The strongest security posture layers multiple controls:

```
Layer 1: Bare Metal        → No shared hardware, no hypervisor attack surface
Layer 2: Container          → Process isolation, filesystem isolation
Layer 3: Firewall           → Network egress control, default-allow; add rules to restrict
Layer 4: Proxy Permissions  → Authentication before HTTP reaches the container
Layer 5: Encrypted FS       → LUKS on every host by default; crypt backend adds a live-host layer
Layer 6: Application        → Field-level encryption, input validation
Layer 7: Snapshots          → Rollback capability, audit via diff
Layer 8: Realms             → API-level tenant isolation
```

Each layer is independently configurable through HTTP, and each can be audited through HTTP. They 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.

This is not defense in depth as a marketing term. It is eight distinct, HTTP-configurable controls between an attacker and your data, each one something you can inspect and change.

---

## What's Next

- **[Building a Full-Stack Application](/guides/full-stack-app/)** -- Build with security from the start
- **[Deploying Autonomous AI Agents](/guides/ai-agents/)** -- AI agents in isolated containers
- **[Proxy Permissions](/foundation/proxy/permissions/)** -- Fine-grained access control
- **[Firewall Configuration](/foundation/networking/firewall/)** -- Network-level security
- **[Encrypted Cloud Storage](/foundation/storage/cloud/)** -- Multi-backend encrypted storage