Dens — a machine your agent keeps
A den is a persistent Linux machine per thread. It sleeps when idle and costs storage only; it wakes when you, a schedule, or an event need it, with files, services and context intact.
Updated 9/16/2026
A den is a real machine your agent keeps between turns — a Linux VM with a persistent disk. Install dependencies, run the app, leave files around: they are there next time. When nobody needs it, it sleeps and you pay for the disk only; the first prompt, portal visit or schedule wakes it (about a minute).
Starting one
On an agent, choose New thread in den. Pick a size (d1.small by
default) and, when the agent has a GitHub connection with a repository
bound, the repository is checked out at /den/repo. The thread's Den
tab shows the status chip, the settings, the attached threads and the
ledger of everything that happened on the machine.
Everything that survives sleep lives under /den:
/den/home the agent's home
/den/repo the checkout, when a repository is bound
/den/.connect0/ den.yaml state, logs (services, setup, commands)
What the agent can do
Five tools are on the belt whenever the thread has a den:
| Tool | What it does |
|---|---|
den_exec | Run a shell command (bash -lc) under /den. Output is capped at 64 KB; a truncated result names the full log to read. |
den_read | Read a text file under /den, optionally a byte range. |
den_write | Write a text file under /den; parent folders are created. |
den_id_token | Mint a fifteen-minute identity token for an audience (below). |
den_schedule | Wake the den on a cron or at an instant and start a run with a prompt (below). |
Paths never leave /den. Every call is a row in the den's ledger.
.connect0/den.yaml
Setup is data, committed with the repository:
version: 1
size: d1.small # d1.small | d1.medium | d1.large | d1.xlarge
setup: # runs once after clone, again when this file changes
- pnpm install
services:
web:
cmd: pnpm dev
port: 3000 # or omit → $PORT is assigned
portal: true # reachable through a portal (below)
secrets: # by reference — the value never touches the disk
DATABASE_URL: connection:9c1e… # a connection id from your Connections page
env:
NODE_ENV: development
idle_minutes: 15
Services are supervised: they start on every wake with $PORT set, are
restarted when they exit, and are left alone after five exits in ten
minutes until the next wake. Their logs are under
/den/.connect0/logs/services/.
Portals
A service declared with portal: true is reachable at
https://<den>-<port>.den.connect0.ai. Anyone on the thread opens it
from the Den tab; the service sees who is looking:
X-Connect0-Authenticated: member=yes; participant=driver; public=no
X-Connect0-Account-Id: …
X-Connect0-User-Id: … (members only)
X-Connect0-User-Email: … (members only)
Drivers can Share publicly… for one hour, three hours, a day or a
week. A public link works without signing in, carries no identity
(member=no; participant=none; public=yes), and can be revoked — it stops
within a minute. A portal visit while the den sleeps shows Waking your
den… and continues when it is up.
Terminal and files
The Den tab has a shared terminal for drivers — one tmux session on
the machine, so two people driving the thread see the same screen — and a
file browser: viewers read, drivers edit. Browsing files does not wake a
sleeping den; the Wake button does.
Workload identity
Instead of copying a cloud credential onto the machine, let the machine
prove who it is. den_id_token (or denctl id-token --audience … inside
the den) mints a fifteen-minute OIDC token:
iss https://auth.connect0.ai/oidc
sub den:<den id>
aud <what you asked for>
connect0: { account_id, project_id?, agent_id, thread_id?, run_id?, den_id, user_id?, email? }
act: { sub: user:<id> } — when a person triggered the run
The issuer's discovery document is
https://auth.connect0.ai/oidc/.well-known/openid-configuration; keys
rotate and the JWKS carries the previous key for the overlap. Every mint is
a row in the den's ledger. Three ways to trust it:
Google Cloud — workload identity federation
gcloud iam workload-identity-pools create connect0 --location=global
gcloud iam workload-identity-pools providers create-oidc dens \
--location=global --workload-identity-pool=connect0 \
--issuer-uri="https://auth.connect0.ai/oidc" \
--attribute-mapping="google.subject=assertion.sub,attribute.account=assertion.connect0.account_id" \
--attribute-condition="assertion.connect0.account_id == '<your account id>'"
gcloud iam service-accounts add-iam-policy-binding den-runner@<project>.iam.gserviceaccount.com \
--role=roles/iam.workloadIdentityUser \
--member="principalSet://iam.googleapis.com/projects/<number>/locations/global/workloadIdentityPools/connect0/attribute.account/<your account id>"
Inside the den:
denctl id-token --audience "//iam.googleapis.com/projects/<number>/locations/global/workloadIdentityPools/connect0/providers/dens" > /tmp/den.jwt
gcloud iam workload-identity-pools create-cred-config \
projects/<number>/locations/global/workloadIdentityPools/connect0/providers/dens \
--service-account=den-runner@<project>.iam.gserviceaccount.com \
--credential-source-file=/tmp/den.jwt --output-file=/tmp/gcp-creds.json
export GOOGLE_APPLICATION_CREDENTIALS=/tmp/gcp-creds.json
AWS — an IAM OIDC identity provider
Create an identity provider with the issuer https://auth.connect0.ai/oidc
and the audience you will request (say sts.amazonaws.com), then a role
whose trust policy matches the den:
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::<account>:oidc-provider/auth.connect0.ai/oidc" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": { "auth.connect0.ai/oidc:aud": "sts.amazonaws.com" },
"StringLike": { "auth.connect0.ai/oidc:sub": "den:*" }
}
}
aws sts assume-role-with-web-identity --role-arn arn:aws:iam::<account>:role/den-runner \
--role-session-name den --web-identity-token "$(denctl id-token --audience sts.amazonaws.com)"
HashiCorp Vault — JWT auth
vault auth enable jwt
vault write auth/jwt/config oidc_discovery_url="https://auth.connect0.ai/oidc" bound_issuer="https://auth.connect0.ai/oidc"
vault write auth/jwt/role/den role_type=jwt user_claim=sub bound_audiences=vault \
bound_claims='{"connect0.account_id":"<your account id>"}' token_policies=den
vault write auth/jwt/login role=den jwt="$(denctl id-token --audience vault)"
Secrets by reference
When a system cannot trust an issuer yet, name a connection in
den.yaml: DATABASE_URL: connection:<id>. At every wake the value is
resolved from your account's connection (a database URL, an API key, a
GitHub installation token) and placed in the environment of the services
and of every command the agent runs. It is never written under /den —
which is why it must not be a file: the file browser shows everything.
Each refresh is a ledger row naming the variables, never the values.
Waking on a schedule
A den can wake itself. The agent sets a rule with den_schedule — a
five-field cron in UTC (no more often than every fifteen minutes) or a
one-off instant, plus the prompt the run should start with:
den_schedule({ cron: "0 * * * *", prompt: "Triage new errors; open a thread per new issue" })
Drivers add, disable, enable and remove rules from the Den tab's Schedule section — the agent's included. When a rule fires, a run starts on its thread, the den wakes, and the ledger says so. At most 24 firings a day per den and ten enabled rules; a rule that would exceed the account's awake-minutes cap is skipped and logged, never run to fail.
Sleeping, waking, cost
A den sleeps after idle_minutes with no run, no command, no portal
request and no terminal. Keep awake holds it up for up to eight hours
at a time. Awake time is billed by the minute at the size's rate; a
sleeping den is billed for its disk only. Per account there are caps on how
many dens exist and how many are awake at once; the Den tab says when one
is reached.
Destroy deletes the machine and its disk; the ledger stays.
What this does not do
- A den is one machine; there is no autoscaling and no multi-machine den.
- A sleeping den still costs its disk; it is not free, it is storage-only.
- Waking takes about a minute; a portal or a prompt that arrives while it sleeps waits for that.
- Public portal links carry no identity and expire; they cannot be made permanent.
- An identity token lives fifteen minutes and cannot be refreshed; ask for a new one.
- A wake rule fires within a minute of its instant, at most 24 times a day, and never more often than every fifteen minutes.
- Secrets by reference are injected as process environment only; they are never written to the disk.
Ask Zero
Ask a question about connect0 and get an answer grounded in the docs, with links to the sources. Signed in? Zero answers with your account in mind.