Sessions run claude with --dangerously-skip-permissions and a prompt built from issue/PR/comment text, so only trusted authors may supply that text. teabot now dispatches a job only when the triggering event's author login is on an allowlist; an empty allowlist dispatches nothing (fail-closed). Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
6.2 KiB
Configuration
teabot reads a single YAML file, by default
$XDG_CONFIG_HOME/teabot/config.yaml (i.e. ~/.config/teabot/config.yaml).
Override the path with --config/-c. Generate a starting point with
teabot config init, and validate the effective settings with
teabot config show.
Top-level keys
| Key | Default | Description |
|---|---|---|
gitea_url |
https://git.unkin.net |
Base URL of the Gitea instance to poll. |
repos |
(required) | List of owner/name repositories to watch. |
poll_interval |
60s |
Delay between poll cycles (Go duration). |
max_concurrent |
2 |
Maximum job containers running at once. |
job_timeout |
30m |
Per-session wall-clock timeout. |
job_image |
git.unkin.net/unkin/agent-dev:latest |
Container image each session runs in. |
container_home |
/home/agent |
Home dir inside job_image (mount target). |
claude_config_dir |
~/.claude |
Host dir with Claude Code credentials. |
anthropic_api_key |
(unset) | If set, injected as ANTHROPIC_API_KEY. |
anthropic_base_url |
(unset) | If set, injected as ANTHROPIC_BASE_URL. |
state_dir |
~/.local/state/teabot |
Where processed-event state is persisted. |
allowed_authors |
(empty → fail-closed) | Gitea usernames whose events may trigger a job. See Author allowlist. |
repo_allowed_authors |
(unset) | Optional per-repo override of allowed_authors, keyed by owner/name. |
personalities |
(required) | Bot identities (see below). |
~ and ~/ are expanded in path-valued keys.
Personalities
A personality is a distinct Gitea bot account. teabot must be able to act as
different identities — e.g. an implementer that opens PRs and a separate reviewer
that critiques them — so each personality points at its own tea config file
rather than sharing your personal ~/.config/tea/config.yml.
personalities:
- name: implementer
tea_config: ~/.config/teabot/tea-implementer.yml
role: implementer # implementer | reviewer | both
git_name: Teabot Implementer
git_email: teabot-implementer@unkin.net
- name: reviewer
tea_config: ~/.config/teabot/tea-reviewer.yml
role: reviewer
git_name: Teabot Reviewer
git_email: teabot-reviewer@unkin.net
| Field | Description |
|---|---|
name |
Label used in logs and prompts. |
tea_config |
Path to a tea config.yml holding this bot's login. teabot reads the API token + username from it and mounts it into the job container. |
role |
implementer (issues), reviewer (pull requests), or both. Defaults to both. |
git_name / git_email |
Commit identity set inside the container. |
Create each tea config with the normal tea workflow, pointing HOME/
XDG_CONFIG_HOME at a scratch dir so it lands somewhere dedicated, or copy an
existing config.yml and edit the token. The file format is exactly tea's own:
logins:
- name: teabot-implementer
url: https://git.unkin.net
token: <bot-api-token>
default: true
user: teabot-implementer
teabot picks the login whose url matches gitea_url, else the default, else
the first. The user field is the bot's username — teabot uses it for loop
prevention (it never reacts to events authored by any personality's username).
At least one personality must be able to implement and at least one to review, or config validation fails.
Author allowlist (security)
Each job runs claude --print --dangerously-skip-permissions inside a container,
and its prompt is built from event text — the issue title/body, PR
description/diff, and comments. That text is attacker-controllable: anyone who
can open an issue or comment on the repo could otherwise inject instructions
into a session that has permissions disabled. The allowlist is the control that
closes this: teabot dispatches a job only when the triggering event's author
login is on the allowlist.
# Global allowlist.
allowed_authors:
- benvin
- trusted-colleague
# Optional per-repo override. A present entry fully REPLACES the global list for
# that repo; an empty list disables dispatch for it; an absent entry falls back
# to the global list.
repo_allowed_authors:
unkin/teabot:
- benvin
Semantics:
- Fail-closed. An empty/absent allowlist dispatches nothing. teabot logs a warning at startup when no allowlist is configured anywhere.
- Events from non-allowlisted authors are recorded as processed (so they don't re-trigger) and logged at info with the author name, but never spawn a container. They are not marked as engaged threads.
- Comment follow-ups are gated on the new comment's author, not just the thread. A comment from a non-allowlisted user on a thread teabot previously acted on is ignored — otherwise an untrusted comment could reopen the injection path on a bot thread.
- Matching is case-insensitive (Gitea usernames are unique case-insensitively).
- This is separate from and in addition to loop prevention: bot personality logins are always excluded first, even if one were listed here.
Claude credentials
By default teabot uses your Claude subscription auth: it copies
claude_config_dir (default ~/.claude) into a per-job scratch dir and mounts
that copy read-write into the container, so the session can refresh tokens
without ever mutating your real config.
Alternatively set anthropic_api_key (and optionally anthropic_base_url for a
gateway such as LiteLLM). When present these are injected as environment
variables and take precedence over the mounted subscription credentials.
SELinux
This host is Fedora, so Docker bind mounts are relabelled with :z. teabot does
this automatically. If you run under a different security model, the relabel
suffix is a field on DockerRunner (SELinuxLabel).
A note on comment follow-ups
teabot only continues comment threads on issues/PRs it acted on itself (opened a PR for, or reviewed). A comment on an unrelated thread is recorded but ignored. Combined with loop prevention (bot-authored comments are skipped), this means teabot will not, for example, keep answering an issue it decided not to implement — it engages a thread only after it has taken an action there.