# Configuration

Enso reads machine-level settings from <code>$ENSO_HOME/config.json</code>. If
<code>ENSO_HOME</code> is unset, the home is <code>~/.enso</code>.

The configuration controls:

- which Slack and Telegram accounts Enso serves;
- which conversations map to which [workspaces](/docs/workspaces/);
- which provider CLIs, models, and effort levels may run;
- interactive timeouts, log rotation, run-history retention, and web-viewer defaults.

<code>config.json</code> is loaded when the service starts. After changing it, validate it
and restart the service. Job definitions are different: <code>JOB.md</code> files are
reloaded by the scheduler every minute. See [Jobs](/docs/jobs/).

## Before editing

<code>config.json</code> is **strict JSON**, not JSONC. Comments, trailing commas,
single-quoted strings, and unquoted keys are invalid.

~~~bash
enso config check
enso config show
enso doctor
~~~

- <code>enso config check</code> reports all configuration problems it can find at once.
- <code>enso config show</code> prints the stored document and redacts object keys ending
  in <code>token</code>. It does not print a normalized or fully defaulted configuration.
- <code>enso doctor</code> also checks provider executables, workspace health, and service
  status.

Enso refuses to serve chat when the configuration has an error. Warnings, such as a
provider executable not currently being found, do not make the JSON invalid, but they
usually indicate the machine is not ready to run.

Protect the file because it contains transport credentials:

~~~bash
chmod 600 "$ENSO_HOME/config.json"
~~~

If <code>ENSO_HOME</code> is not set in your shell, use
<code>~/.enso/config.json</code> instead.

## A minimal configuration

This is valid strict JSON after replacing the example values. The
<code>default</code> workspace must already exist.

~~~json
{
  "version": 1,
  "transports": {
    "telegram": {
      "bot_token": "REPLACE_WITH_TELEGRAM_BOT_TOKEN",
      "allowed_users": ["123456789"],
      "notify": "123456789"
    }
  },
  "bindings": {
    "telegram:123456789": "default"
  },
  "defaults": {
    "provider": "claude",
    "model": "sonnet",
    "effort": "high"
  },
  "providers": {
    "claude": {
      "path": "claude",
      "models": ["opus", "sonnet", "haiku"],
      "args": ["--dangerously-skip-permissions"]
    }
  }
}
~~~

Create the workspace if needed, then validate:

~~~bash
enso workspace create default
enso config check
~~~

## Complete example

This example shows every top-level section. It is strict JSON and contains no comments;
replace tokens, IDs, paths, model choices, and permission flags for your machine.

~~~json
{
  "version": 1,
  "transports": {
    "slack": {
      "bot_token": "xoxb-REPLACE_ME",
      "app_token": "xapp-REPLACE_ME",
      "notify": "C0123456789",
      "mention_required": false,
      "thread_mention_required": false
    },
    "telegram": {
      "bot_token": "REPLACE_ME",
      "allowed_users": ["123456789"],
      "notify": "123456789"
    }
  },
  "bindings": {
    "slack:dm:U0123456789": "default",
    "slack:C0123456789": "default",
    "slack:C9876543210": "research",
    "telegram:123456789": "default"
  },
  "defaults": {
    "provider": "claude",
    "model": "sonnet",
    "effort": "high"
  },
  "workspaces": {
    "research": {
      "agent": {
        "provider": "codex",
        "model": "sol",
        "effort": "xhigh"
      }
    },
    "default": {
      "providers": {
        "claude": {
          "args": ["--permission-mode", "dontAsk"]
        }
      }
    }
  },
  "providers": {
    "claude": {
      "path": "claude",
      "models": ["opus", "sonnet", "haiku"],
      "args": ["--dangerously-skip-permissions"]
    },
    "codex": {
      "path": "codex",
      "models": ["sol", "terra", "luna"],
      "args": ["--dangerously-bypass-approvals-and-sandbox"]
    },
    "grok": {
      "path": "grok",
      "models": ["grok-4.6", "grok-4.5"],
      "args": ["--always-approve"]
    }
  },
  "agent": {
    "timeout": 3600
  },
  "logging": {
    "level": "INFO",
    "max_bytes": 10485760,
    "backups": 5
  },
  "runs": {
    "keep": 500,
    "max_age_days": 30
  },
  "web": {
    "host": "127.0.0.1",
    "port": 8787
  }
}
~~~

## Required and optional sections

| Key | Required | Contract |
| --- | --- | --- |
| <code>version</code> | Yes | Must currently be the integer <code>1</code>. |
| <code>transports</code> | Yes | Configure Slack, Telegram, or both. At least one is required. |
| <code>providers</code> | Yes | A non-empty object containing supported provider names. |
| <code>defaults</code> | Yes | A complete provider/model/effort triple for interactive conversations. |
| <code>bindings</code> | No | Conversation-to-workspace mappings. An empty object is allowed. |
| <code>workspaces</code> | No | Agent or provider-argument overrides for selected workspaces. |
| <code>agent</code> | No | Interactive execution settings. |
| <code>logging</code> | No | Log level and rotation. |
| <code>runs</code> | No | Finished job-run retention. |
| <code>web</code> | No | Default bind host and port for the viewer. |

Unknown nested behavior should not be assumed to exist merely because JSON accepts an
extra key. Use [the CLI reference](/docs/cli/) and <code>enso config check</code> as the
public contract.

## Transports

Slack and Telegram can run together in the same Enso process.

### Slack

~~~json
{
  "transports": {
    "slack": {
      "bot_token": "xoxb-REPLACE_ME",
      "app_token": "xapp-REPLACE_ME",
      "notify": "C0123456789",
      "mention_required": false,
      "thread_mention_required": false
    }
  }
}
~~~

Slack requires <code>bot_token</code> and <code>app_token</code>. The app token is used
for Socket Mode.

The optional <code>notify</code> value is a **bare** Slack conversation ID beginning with
<code>C</code>, <code>G</code>, or <code>D</code>. It is the transport's fallback target
for job failure alerts and untargeted <code>enso message send</code> calls.

<code>mention_required</code> controls whether a top-level channel message must mention
the bot. <code>thread_mention_required</code> applies separately to replies inside a
thread. Both default to <code>false</code> and must be JSON booleans.

Invite the bot to every channel you bind. A valid binding does not add the bot to Slack
for you.

### Telegram

~~~json
{
  "transports": {
    "telegram": {
      "bot_token": "REPLACE_ME",
      "allowed_users": ["123456789", 987654321],
      "notify": "123456789"
    }
  }
}
~~~

Telegram is limited to private chats. <code>allowed_users</code> is a list of exact,
positive numeric Telegram user IDs; JSON integers and canonical decimal strings are
accepted. Anyone outside the list is ignored silently. An empty or omitted list therefore
authorizes nobody.

The optional <code>notify</code> target is one positive Telegram user ID. It may be an
integer or a canonical decimal string, although strings are often easier to handle
consistently.

### Notification destination forms

Transport-level <code>notify</code> values are bare IDs. Job and CLI destinations may be:

| Destination | Example |
| --- | --- |
| Explicit Slack | <code>slack:C0123456789</code> |
| Explicit Telegram | <code>telegram:123456789</code> |
| Bare ID | <code>C0123456789</code> or <code>123456789</code>, only when exactly one transport is configured |

With both transports configured, always prefix job and CLI destinations so the target is
unambiguous.

## Conversation bindings

A binding maps a transport conversation to a workspace:

| Binding key | Conversation |
| --- | --- |
| <code>slack:C…</code> or <code>slack:G…</code> | Slack channel; each top-level message starts a thread and conversation |
| <code>slack:dm:U…</code> | Direct messages with one Slack user |
| <code>telegram:&lt;user-id&gt;</code> | A Telegram private chat |

~~~json
{
  "bindings": {
    "slack:C0123456789": "research",
    "slack:dm:U0123456789": "default",
    "telegram:123456789": "default"
  }
}
~~~

Workspace names are lowercase kebab-case and at most 64 characters. Every bound workspace
directory must exist at <code>$ENSO_HOME/workspaces/&lt;name&gt;</code>; a missing directory
is a configuration error. Create it with:

~~~bash
enso workspace create research
~~~

A binding for an unconfigured transport is a warning. It is usually stale configuration
and cannot receive messages until that transport is configured. Bindings take effect only
after the service restarts.

See [Workspaces](/docs/workspaces/) for layout, instructions, uploads, and auditing.

## Providers

Enso currently recognizes three provider keys: <code>claude</code>,
<code>codex</code>, and <code>grok</code>.

Each configured provider has:

| Field | Meaning |
| --- | --- |
| <code>path</code> | Executable path. A leading <code>~</code> is expanded; a bare command is resolved from <code>PATH</code>. If omitted, the provider name is used. |
| <code>models</code> | Non-empty list of model names that configuration and jobs may select. |
| <code>args</code> | String arguments appended verbatim to every invocation. Defaults to an empty list. |

The initial setup choices use these model names and effort vocabularies:

| Provider | Initial model names | Accepted effort levels |
| --- | --- | --- |
| Claude | <code>opus</code>, <code>sonnet</code>, <code>haiku</code> | <code>low</code>, <code>medium</code>, <code>high</code>, <code>xhigh</code>, <code>max</code> |
| Codex | <code>sol</code>, <code>terra</code>, <code>luna</code> | <code>low</code>, <code>medium</code>, <code>high</code>, <code>xhigh</code>, <code>max</code>, <code>ultra</code> |
| Grok | <code>grok-4.6</code>, <code>grok-4.5</code> | <code>low</code>, <code>medium</code>, <code>high</code>, <code>xhigh</code> |

Codex's setup aliases map <code>sol</code>, <code>terra</code>, and <code>luna</code> to
their corresponding installed Codex model IDs. The <code>models</code> list controls what
Enso accepts; it does not prove that the installed provider CLI can actually use a name.
Use <code>enso doctor</code> and test the provider on the machine.

Enso clamps an effort **downward** when it exceeds the known maximum for a selected
model, and logs the change. It does not silently find another model.

### Permissions and authentication

Enso delegates authentication, sessions, filesystem access, and approval behavior to the
provider CLI. Provider <code>args</code> are not interpreted or made safer by Enso. The
unattended flags shown in setup and in the example grant broad authority:

- Claude: <code>--dangerously-skip-permissions</code>
- Codex: <code>--dangerously-bypass-approvals-and-sandbox</code>
- Grok: <code>--always-approve</code>

Choose those flags deliberately. A workspace is a working directory and context boundary,
not an operating-system sandbox. A provider may be able to read or change files and use
credentials elsewhere on the machine. Treat every enabled provider, workspace skill, job
script, and secret as part of one trusted automation system.

## Defaults and workspace overrides

<code>defaults</code> is one complete interactive-agent selection:

~~~json
{
  "defaults": {
    "provider": "claude",
    "model": "sonnet",
    "effort": "high"
  }
}
~~~

All three fields are required. The provider must be configured, the model must appear in
that provider's <code>models</code> list, and effort must be valid for that provider.

A workspace may replace the complete triple:

~~~json
{
  "workspaces": {
    "research": {
      "agent": {
        "provider": "codex",
        "model": "sol",
        "effort": "xhigh"
      }
    }
  }
}
~~~

It may also replace one configured provider's arguments:

~~~json
{
  "workspaces": {
    "default": {
      "providers": {
        "claude": {
          "args": ["--permission-mode", "dontAsk"]
        }
      }
    }
  }
}
~~~

These are replacement rules:

- a workspace <code>agent</code> block replaces the whole default triple and must include
  provider, model, and effort;
- <code>workspaces.&lt;name&gt;.providers.&lt;provider&gt;.args</code> replaces that
  provider's global argument list for the workspace; it is not appended to it.

Only workspaces that need overrides belong in the <code>workspaces</code> configuration
object.

[Jobs](/docs/jobs/) do **not** inherit this interactive default triple. Every
<code>JOB.md</code> explicitly declares its provider, model, effort, and workspace.
Workspace-specific provider arguments still apply when that provider runs in the named
workspace.

## Interactive timeout

~~~json
{
  "agent": {
    "timeout": 3600
  }
}
~~~

<code>agent.timeout</code> is the interactive-turn timeout in seconds. It defaults to
<code>3600</code> and must be a non-negative integer. A value of <code>0</code> disables
the interactive timeout. Jobs use their own required/defaulted timeout field instead.

## Secrets and environment

Provider credentials and values used by job scripts can live in:

~~~text
$ENSO_HOME/secrets/*.env
~~~

Create and protect the directory:

~~~bash
mkdir -p "$ENSO_HOME/secrets"
chmod 700 "$ENSO_HOME/secrets"
chmod 600 "$ENSO_HOME/secrets/"*.env
~~~

Each file contains simple assignments:

~~~dotenv
GITHUB_TOKEN=replace-me
export ANOTHER_TOKEN="replace-me-too"
~~~

This parser is intentionally small:

- blank lines and lines without <code>=</code> are ignored;
- an optional leading <code>export </code> is removed;
- lines whose parsed key begins with <code>#</code> are ignored;
- a variable already present in the service environment wins;
- surrounding whitespace is stripped, followed by any leading or trailing single- or
  double-quote characters;
- the file is **not a shell script**: no variable expansion, command substitution, or shell
  escaping is performed.

Secrets are loaded when Enso starts and inherited by provider processes and job scripts.
Restart after changing them. There is no per-workspace secret boundary: all agents and
scripts launched by the service share the environment.

Transport bot tokens are required directly in <code>config.json</code>; the JSON loader
does not substitute environment-variable references into those fields.

## Logs and run retention

Defaults:

~~~json
{
  "logging": {
    "level": "INFO",
    "max_bytes": 10485760,
    "backups": 5
  },
  "runs": {
    "keep": 500,
    "max_age_days": 30
  }
}
~~~

<code>logging.level</code> is <code>DEBUG</code>, <code>INFO</code>,
<code>WARNING</code>, or <code>ERROR</code>; case is normalized. The byte and backup
limits, along with both run-retention values, must be non-negative integers.

Finished run rows are pruned if they are older than <code>max_age_days</code> **or**
outside the newest <code>keep</code> rows. Rows still marked <code>running</code> are not
pruned by that rule.

Run history retains only the final 1 MiB of a job's provider output in
<code>enso.db</code>. That output is **not automatically written into the workspace**.
If durable output belongs in <code>drafts/</code> or <code>knowledge/</code>, instruct the
agent to write it there or use a postrun script. See [Jobs](/docs/jobs/#postrun-scripts).

Use <code>DEBUG</code> logging carefully on a multi-user machine: verbose provider and
routing details can reveal more operational context than normal logs.

## Web viewer

~~~json
{
  "web": {
    "host": "127.0.0.1",
    "port": 8787
  }
}
~~~

These values are defaults for <code>enso web start</code>; command-line flags override
them. <code>host</code> must be a non-empty string. <code>port</code> must be an integer
from 1 through 65535.

The viewer can still start with its safe defaults when <code>config.json</code> is missing
or invalid, so its Health page can show the problem. It is read-only and has no built-in
authentication. Keep it on <code>127.0.0.1</code> unless a trusted access layer protects
it. See [Web viewer](/docs/web/).

## Chat commands

Slack commands use <code>!</code>; Telegram commands use <code>/</code>.

| Command | Effect |
| --- | --- |
| <code>stop</code> | Stops the process running for this conversation and drops its queued turns. |
| <code>clear</code> | Forgets stored provider sessions for the conversation, so the next turn starts fresh. |
| <code>status</code> | Shows the effective workspace and agent, session age, active process, and queue depth. |
| <code>help</code> | Lists commands. |
| <code>restart</code> | Replies, then restarts the service or re-executes <code>enso serve</code>. |

Session state is keyed by conversation and provider and can only resume in the workspace
where it began. Idle session database entries are pruned after 30 days when the service
starts. Clearing or invalidating a session may also remove provider-owned local session
artifacts for providers that expose them; it is more than a visual chat-history reset.

## Safe edit procedure

1. Make sure every workspace named by a binding exists.
2. Edit <code>$ENSO_HOME/config.json</code> as strict JSON.
3. Run <code>enso config check</code>.
4. Run <code>enso doctor</code> if providers, paths, or workspaces changed.
5. Restart Enso.
6. Send <code>status</code> in the affected chat and confirm the effective workspace,
   provider, model, and effort.

## Troubleshooting

### JSON fails before schema validation

Remove comments and trailing commas, use double quotes, and validate the raw document:

~~~bash
python -m json.tool "$ENSO_HOME/config.json" >/dev/null
enso config check
~~~

### A binding says its workspace is missing

The configuration deliberately fails closed when there is nowhere to run:

~~~bash
enso workspace create research
enso workspace audit research
enso config check
~~~

### The provider path is not executable

Confirm the same environment that starts Enso can resolve the command:

~~~bash
command -v claude
enso doctor
~~~

A terminal's <code>PATH</code> may differ from a background service's. An absolute
provider path avoids that ambiguity.

### A workspace still uses global provider arguments

Check that the workspace name exactly matches the binding or job, and remember that the
override must contain an <code>args</code> array:

~~~json
{
  "workspaces": {
    "research": {
      "providers": {
        "codex": {
          "args": ["--dangerously-bypass-approvals-and-sandbox"]
        }
      }
    }
  }
}
~~~

Restart after changing <code>config.json</code>.

### Alerts are not delivered

Check, in order:

1. the job's explicit <code>notify</code> value;
2. the configured transports' <code>notify</code> values;
3. whether an explicitly prefixed destination names a configured transport;
4. whether the Slack bot is in the target channel or the Telegram user has started the bot
   and can receive its outbound message.

Successful and <code>no_work</code> jobs are intentionally silent unless the prompt sends
a message itself.

## Machine-readable checklist

An agent editing Enso configuration should preserve these invariants:

- emit strict JSON with <code>version: 1</code>;
- configure at least one transport and at least one supported provider;
- keep every agent selection a complete provider/model/effort triple;
- use only models declared for that provider and valid provider effort strings;
- create each bound workspace before referencing it;
- treat workspace provider arguments as replacements;
- restart for configuration or secret changes;
- never describe a workspace as a security sandbox;
- never assume retained job stdout was saved as a workspace file.

Continue with [Workspaces](/docs/workspaces/), [Jobs](/docs/jobs/), or the
[CLI reference](/docs/cli/).
