> ## Documentation Index
> Fetch the complete documentation index at: https://celly.agub.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration

> The full environment variable reference for Celly. Two values are required, everything else has a default.

`.env` is the single source of truth. It is loaded automatically at startup
(`process.loadEnvFile`), is gitignored, and is validated at boot. A missing or
invalid required value fails fast with an actionable error.

Start from `.env.example` and set the required values. Everything else has a
working default:

```dotenv theme={null}
DISCORD_TOKEN=your-bot-token
DISCORD_GUILD_IDS=111111111111111111,222222222222222222
```

## Variables

| Variable                | Default             | Purpose                                                                                                                                              |
| ----------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DISCORD_TOKEN`         | **required**        | Bot token.                                                                                                                                           |
| `DISCORD_GUILD_IDS`     | **required**        | Comma-separated guild IDs. Entries are trimmed, blanks dropped, and duplicates ignored.                                                              |
| `DISCORD_GUILD_ID`      | unset               | Legacy single-guild form, used only when `DISCORD_GUILD_IDS` is unset.                                                                               |
| `PROJECTS_ROOT`         | `~/Celly/projects`  | Allowed project root; created on boot.                                                                                                               |
| `ACCESS_ROLE_ID`        | unset               | Role **ID** allowed to use the bot.                                                                                                                  |
| `BLOCK_ROLE_ID`         | unset               | Role **ID** denied access (checked first).                                                                                                           |
| `OWNER_ROLE_ID`         | unset               | Role **ID** allowed to run owner-only `/project` mutations. The guild owner is always allowed.                                                       |
| `CATEGORY_ID`           | auto-create `Forge` | Discord category for project channels.                                                                                                               |
| `SANDBOX_TEMPLATE`      | `opencode`          | `sbx create` agent/template.                                                                                                                         |
| `SANDBOX_CPUS`          | `2`                 | Sandbox CPU limit.                                                                                                                                   |
| `SANDBOX_MEMORY`        | `4g`                | Sandbox memory limit.                                                                                                                                |
| `PORT_RANGE_START`      | `4300`              | Host loopback port pool start.                                                                                                                       |
| `PORT_RANGE_END`        | `4399`              | Host loopback port pool end (must exceed the start).                                                                                                 |
| `DEFAULT_MODEL`         | unset               | Seeded into `settings` on first boot only.                                                                                                           |
| `DEFAULT_AGENT`         | unset               | Seeded into `settings` on first boot only.                                                                                                           |
| `APPROVAL_MODE`         | `buttons`           | `auto`, `buttons`, or `plan`. Seeded into `settings.approval_mode` on first boot; `/mode` writes a per-channel `approval_mode:<channelId>` override. |
| `BOOT_TIMEOUT_MS`       | `120000`            | Create-saga health wait.                                                                                                                             |
| `HEALTH_TIMEOUT_MS`     | `30000`             | Wake health wait.                                                                                                                                    |
| `EDIT_INTERVAL_MS`      | `1200`              | Render throttle floor.                                                                                                                               |
| `ATTACHMENT_MAX_BYTES`  | `102400`            | Text-attachment cap (100 KB).                                                                                                                        |
| `MAX_QUEUE`             | `20`                | Per-thread prompt queue bound.                                                                                                                       |
| `MAX_CONCURRENT_RUNS`   | `4`                 | Global concurrent run cap.                                                                                                                           |
| `ATTACH_AUTO_THREAD`    | `false`             | Auto-create a Discord thread for a terminal-started session on its first event.                                                                      |
| `IDLE_STOP_MINUTES`     | `0`                 | Stop a project after this many minutes without activity. `0` disables.                                                                               |
| `SESSION_BUDGET_USD`    | `0`                 | Per-session cost budget in USD; `0` disables. Per-channel override with `/budget set`.                                                               |
| `DATA_DIR`              | `./data`            | SQLite database, logs, and lock file.                                                                                                                |
| `LOG_LEVEL`             | `info`              | One of `debug`, `info`, `warn`, `error`.                                                                                                             |
| `ADMIN_PORT`            | `4560`              | Loopback-only admin page; `0` disables.                                                                                                              |
| `LOG_MAX_BYTES`         | `5000000`           | Rotate a log file after this many bytes.                                                                                                             |
| `LOG_MAX_FILES`         | `3`                 | Rotated log copies kept (`.1`…`.N`).                                                                                                                 |
| `BACKUP_INTERVAL_HOURS` | `24`                | SQLite backup cadence; `0` disables.                                                                                                                 |
| `BACKUP_KEEP`           | `7`                 | Backup files kept before pruning.                                                                                                                    |

<Note>
  `DEFAULT_MODEL`, `DEFAULT_AGENT`, and `APPROVAL_MODE` are seeded into the
  `settings` table on the first boot only. Once a value exists there, it is
  authoritative and re-reading `.env` will not overwrite it.
</Note>

## Multiple guilds

Celly deploys its command set to every guild in `DISCORD_GUILD_IDS` and runs
boot subscribe and thread reconcile across all of them. At startup each
configured guild is fetched; a guild the bot cannot see (for example, it was
never invited) is skipped with a warning. Startup fails only when **no**
configured guild is reachable, or when the bot token itself is invalid. A
single guild failing command deployment does not abort startup.

`Config.guildId` remains available as `guildIds[0]` for code paths that still
expect one guild.

<Note>
  Access control is global: `ACCESS_ROLE_ID`, `BLOCK_ROLE_ID`, and
  `OWNER_ROLE_ID` apply to every configured guild. Role IDs only match in the
  guild that owns them, and each guild's owner (or a member with `Manage Guild` /
  `Administrator`) is always allowed in that guild. Per-guild role configuration
  is not supported in this version.
</Note>

`SESSION_BUDGET_USD` is the global seed. `/budget set <usd>` stores
`budget_usd:<channelId>` in the `settings` table; that value wins for the
channel, and `/budget set 0` disables the budget for it.

## `PROJECTS_ROOT` and path containment

`PROJECTS_ROOT` is the only directory Celly will mount. A project's host
directory must resolve (realpath, case-insensitive) to a path **inside**
`PROJECTS_ROOT`. Anything outside is rejected before a sandbox is created.

On top of containment, a sensitive-path denylist rejects any project directory
that overlaps:

* The bot repository itself (`process.cwd()`).
* `DATA_DIR`.
* Under the user profile: `.ssh`, `.aws`, `.gnupg`, `.config`, `.docker`,
  `.kube`, `.azure`, `.npmrc`, `.netrc`, `.celly`, and `AppData`.
* System directories (`C:\Windows`, `System32`, `Program Files`,
  `ProgramData`, or `/etc`, `/usr`, `/bin`, `/sbin`, `/var`, `/opt`, `/System`,
  `/Library` on POSIX).

Because containment is checked both ways, you also cannot register a parent of
`PROJECTS_ROOT` or of any denied path. `PROJECTS_ROOT` is intentionally **not**
runtime-editable; changing it means editing `.env` and restarting.

<Warning>
  Keep secrets out of project directories. A mounted project is treated as
  untrusted content, and the agent can read anything inside it (`git` remotes,
  `.env` files, tokens). Use the sandbox credential proxy instead of committing
  credentials.
</Warning>

## `DATA_DIR`

`DATA_DIR` holds the SQLite database (`bot.db`), the rotating log
(`bot.log`), per-project server logs (`logs/<sandbox>.log`), and the
single-instance lock. It is gitignored and created on boot.

<Warning>
  Keep `DATA_DIR` outside OneDrive/Dropbox-style synced folders. Cloud-sync
  detection is not implemented in v1, so this is on you.
</Warning>

## Idle auto-stop

With `IDLE_STOP_MINUTES` set to a value greater than `0` (default `0`,
disabled), Celly stops a project's sandbox after that many minutes without
message, prompt, or `!shell` activity. Projects with a run in flight and
projects still provisioning are skipped. Celly posts a plain notice in the
project channel; the next message wakes the project again. Leave
`IDLE_STOP_MINUTES=0` to keep every started project running.

## Admin page, backups, and log rotation

`ADMIN_PORT` (default `4560`, `0` disables) starts an HTTP status page and JSON
API that binds **`127.0.0.1` only**. It is **local by design and
unauthenticated**: there is no login or token, and it is reachable only from the
machine running Celly at `http://127.0.0.1:<ADMIN_PORT>`. It is not reachable
from other devices or the network. Because it trusts anything on the host, never
bind it to `0.0.0.0` and never publish or port-forward it. For remote access,
forward the loopback port over SSH:

```sh theme={null}
ssh -L 4560:127.0.0.1:4560 user@host
```

then open `http://127.0.0.1:4560` on your local machine. Set `ADMIN_PORT=0` to
disable the page entirely.

Backups run every `BACKUP_INTERVAL_HOURS` hours into `DATA_DIR/backups` and keep
`BACKUP_KEEP` files. Logs rotate at `LOG_MAX_BYTES` and keep `LOG_MAX_FILES`
numbered copies.
