This document covers running Babbla as an always-on service: locally (no container), locally via Docker Compose, and on Eyevinn OSC for persistent hosting.
Babbla is a single long-lived Python process (babbla.app). It connects to Slack over
Socket Mode, which means it opens an outbound WebSocket — no inbound port, no load
balancer, no TLS termination required. State (conversation history, per-thread context) is
stored in a SQLite database at the path set by BABBLA_DB (default babbla.db beside the
working directory; on the container it is on a named Docker volume at /state/babbla.db).
A separate one-shot command (babbla.digest --once) generates and posts the weekly digest;
it exits after sending and can be triggered by a cron job or a scheduled container run.
| Variable | Required | Purpose |
|---|---|---|
SLACK_BOT_TOKEN |
Yes | Slack bot token (xoxb-…) |
SLACK_APP_TOKEN |
Yes | Slack app-level token (xapp-…, Socket Mode) |
GITHUB_TOKEN |
Yes | Fine-grained PAT, read-only, scoped to the target repo |
ANTHROPIC_API_KEY |
Optional locally / required on OSC | Claude API key; omit on a developer laptop to use a Claude subscription (Path B) |
BABBLA_GITHUB_MCP |
Set to binary in the image |
Selects the bundled github-mcp-server binary instead of pulling via Docker |
All secrets are injected at runtime — never baked into the image or committed to the repository.
Babbla has two tiers: the Ask tier (interactive Asks, scheduled digests, quiz, and ADR runs)
and the Classifier tier (lobby routing and personal-intent — pure label-emitters). Set
BABBLA_MODEL to change the shared default for both tiers; existing deployments that don't set
it are unchanged. Each tier can then be tuned independently with four optional knobs:
BABBLA_ASK_MODEL=... # overrides BABBLA_MODEL for the Ask tier
BABBLA_ASK_EFFORT=high # low|medium|high|xhigh|max
BABBLA_ASK_FALLBACK_MODEL=...
BABBLA_ASK_MAX_TURNS=16
BABBLA_ASK_MAX_BUDGET_USD=2.0
BABBLA_CLASSIFIER_MODEL=... # overrides BABBLA_MODEL for the Classifier tier
BABBLA_CLASSIFIER_EFFORT=low
BABBLA_CLASSIFIER_FALLBACK_MODEL=
BABBLA_CLASSIFIER_MAX_TURNS=1
BABBLA_CLASSIFIER_MAX_BUDGET_USD=
All settings are optional and inert until set. See .env.example for the
full commented block. Run babbla-doctor (or python -m babbla.doctor) to see the resolved
tiers.
The token must be a fine-grained Personal Access Token scoped to the target repository (or organisation) with the following read-only permissions:
| Scope | Needed for |
|---|---|
contents (read) |
Fetching files, commits, and branches |
metadata (read) |
Repository metadata (always required for fine-grained PATs) |
actions (read) |
Workflow run digests (deploy digest type only) |
For a plain Q&A or branch-type digest, contents + metadata is sufficient. Add
actions:read only if you configure a deploy-type channel in config/channels.yaml.
This is the fastest path on a developer laptop that already has a Claude Code CLI subscription.
The subscription credential is used automatically — no ANTHROPIC_API_KEY needed.
# 1. Install dependencies (once)
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
# 2. Load secrets from .env
set -a && source .env && set +a
# 3. Start the always-on bot
.venv/bin/python -m babbla.app
# 3b. (Separate terminal) Run a one-shot digest manually
.venv/bin/python -m babbla.digest --onceconfig/channels.yaml maps Slack channel IDs to GitHub projects. Edit it to point at your
target repo before starting.
The docker-compose.yml at the repo root builds the image locally (which includes the bundled
github-mcp-server binary) and injects secrets from .env:
docker compose up --buildThe compose file mounts ./config as read-only at /data (so channels.yaml is picked up)
and stores the SQLite database on a separate named volume (babbla-state) at
/state/babbla.db. This keeps the config mount read-only while giving SQLite a writable
location.
To tail logs: docker compose logs -f babbla
To stop: docker compose down (the babbla-state volume persists; add -v to also remove it)
The steps below use placeholder values; substitute your real project/service identifiers.
docker build -t ghcr.io/<org>/babbla:latest .
docker push ghcr.io/<org>/babbla:latestIn the OSC console (or via the OSC CLI), create an Always-On service instance using the pushed image. Always-On instances restart automatically on failure — no external watchdog needed.
Configure the following environment variables in the OSC service settings (not in source control):
SLACK_BOT_TOKEN=xoxb-…
SLACK_APP_TOKEN=xapp-…
GITHUB_TOKEN=github_pat_…
ANTHROPIC_API_KEY=sk-ant-…
BABBLA_GITHUB_MCP=binary
BABBLA_CONFIG=/data/channels.yaml
BABBLA_DB=/state/babbla.db
Attach two volumes to the service:
| Mount path | Purpose |
|---|---|
/data |
channels.yaml configuration (read-only recommended) |
/state |
SQLite database (babbla.db) — must be writable |
Upload your config/channels.yaml to the /data volume before the first start. The Slack
channel mapping must include your channel IDs (C0XXXXXXXXX) and GitHub repo targets.
After deploy, verify in the OSC console that exactly one instance shows status Running.
Check the service logs for the [APP] connected to Slack line. Send a test mention in the
configured channel (C0XXXXXXXXX) and confirm a reply arrives.
Users manage their subscriptions by DMing Babbla in plain language — "follow MyTV", "what am I following?", "make my digest weekly", "only show me security in MyTV". This works out of the box; no Slack-app change is required (the natural-language intent classifier is always wired).
The Personal Digest (delivered by DM) additionally requires a personal_digest: block in
config/channels.yaml (see the commented example there).
Optional
/babblaslash command. The code also ships an equivalent/babbla subscribe|unsubscribe|list|digesthandler, but it does not fire unless you register a/babblaslash command in the Slack app (Socket Mode, no request URL; adds thecommandsscope). It is redundant with the plain-language DM path above, so registering it is optional.
files:writescope (per-project skills): if any project binding declares askills:list, the bot token additionally needs thefiles:writeOAuth scope. Babbla uses it to upload skill-produced artifacts back to the asking surface viafiles_upload_v2. Without this scope the ask still answers normally — the artifact upload degrades to a logged no-op rather than a failed interaction.
groups:readscope (private personal subscriptions, ADR 0017): a user may follow / DM-ask / topic-filter / receive-in-digest a private project only while they are a live member of its bound private channel. Babbla verifies this with a Slackconversations.membersread, which needs thegroups:readOAuth scope to see private-channel membership (Babbla must also be a member of the channel — it already is, to receive mentions). The check is fail-closed: without this scope (or on any lookup error) the membership probe returns "not a member", so private content is simply withheld — never leaked. Public/internal projects are unaffected and incur no membership lookup.
The Agent SDK always drives the claude CLI subprocess — true for plain Q&A and
for skills alike. So per-project skills add no new runtime; they reuse the
same SDK→CLI path. Most of the container checklist is required for any Babbla
deploy, not just skills:
Required for all of Babbla (not skills-specific):
- Bundle the
claudeCLI in the image (the SDK shells out to it; it is not pure-Python). Pin a version compatible with the installedclaude-agent-sdk. - Auth, one of: mount the Path-B subscription credentials into the
container's
$HOME/.claude(read-only is fine), or setANTHROPIC_API_KEY(Path A — already supported;ANTHROPIC_API_KEYis intentionally optional inapp.py). - GitHub MCP launcher: set
BABBLA_GITHUB_MCP=binaryand install thegithub-mcp-serverbinary in the image, so Babbla does not try todocker runthe MCP server from inside its own container (which would need Docker-in-Docker). The skilled path reuses the samemcp_servers, so this one setting covers both. - Persist
CLAUDE_CONFIG_DIR(default~/.claude) on a writable volume that survives restarts: thread-scoped conversation resume (ADR 0013) reads the CLI's session transcripts from there. This matters for skilled and non-skilled threads.
Skills-specific (small):
- Writable scratch: skills write to a per-thread scratch dir under
$TMPDIR. On a--read-onlycontainer, mount atmpfsand pointTMPDIRat it. The scratch is wiped per ask, so it can be fully ephemeral. - Bake
config/skills/into the image (likeconfig/channels.yaml), or setBABBLA_SKILLS_POOLto a mounted path. An unknown skill name fails fast at config load, so a missing pool is loud, not silent. - Slack
files:writescope (above) for artifact upload.
Isolation gets better in a container: a clean image has no operator
~/.claude/CLAUDE.md or user-global skills, so the skilled path's
setting_sources=["project"] from a fresh scratch is airtight by construction.
On a developer laptop, Babbla can authenticate to Claude through the Claude Code CLI
subscription (Path B) without any ANTHROPIC_API_KEY. This works because the Claude Agent
SDK falls back to the local CLI credential when no API key is present.
This credential is laptop-bound — it lives in the local CLI session and cannot be
transferred to a server. For OSC (or any remote host), you must inject a dedicated
ANTHROPIC_API_KEY as a secret. Using the subscription credential as a server secret (e.g.,
by exporting the CLI token) is a fragile interim that will break on token rotation and is not
supported.
In short: local development → subscription (no key needed); OSC/server → inject
ANTHROPIC_API_KEY.