Team / group mode
aipager runs by default as a 1:1 DM bot — you and the bot, no one
else. Team mode opens it up to a Telegram group with multiple
developers, the way teams use @gitbot or @deploybot: anyone on
the allow-list can mention @aipagerbot to inject prompts, approve
permission requests, or check status.
Team mode is opt-in. Personal-mode installs are unaffected.
Decide carefully
Adding a Telegram user to the team gives them code-execution rights on the host running the daemon. They can:
- Inject prompts that claude turns into shell commands, file edits, network calls.
- Approve / deny tool calls — your
~/.claude/settings.jsonstill decides which tools claude asks about, but anyone whose role can approve can hit Allow. - Create / kill / switch sessions.
Treat the allow-list the same way you treat SSH access to the
machine. The audit log
(~/.claude/aipager-audit.jsonl) records
who did what so you can review later, but it's after-the-fact.
Setup
Run aipager config. The wizard adapts:
- No config yet → first-run wizard. Picks mode upfront, then walks token → mode → chat (group or DM) → members + roles (if team) → deps → settings → write.
- Config exists → edit menu. Opens a current-state panel and offers focused actions: add a user, remove a user, change a user's role, edit deny rules, switch mode, refresh the bot token, or run the full setup again.
The wizard writes everything to ~/.config/aipager/aipager.yaml
(mode 0600): the bot token, the chat(s) the bot serves, and each
chat's members with their roles. What each role may do lives in
the user-owned ~/.config/aipager/policy.yaml — see
Rules below. Installs configured with the older
team.yaml format keep working and are migrated automatically —
by the daemon at startup, or by the wizard on its next run.
For the group chat ID you can paste it manually, or pick
"Auto-detect" and let the wizard watch for a /start in the group
(add the bot first). The same auto-detect works for member user IDs
— the wizard captures the next message's sender id and suggests
their Telegram username as the label.
Live reload
The wizard signals the running daemon via SIGUSR1 after every
config change, so add-user / remove-user / change-role / edit-rules
/ switch-mode apply without a daemon restart. The reload covers
aipager.yaml, policy.yaml, and legacy team.yaml alike.
Restart is still required when changes affect the bot token or the chat id the bot polls. The wizard distinguishes between hot-reloadable and restart-needed changes and prints the appropriate hint.
To trigger a reload manually (e.g. after a hand-edit):
kill -USR1 $(pgrep -f 'aipager start')
If a config file is malformed at reload time, the daemon logs a WARN and keeps the previous in-memory config — so you can't lock yourself out by typo'ing a hand-edit.
Also, on @BotFather, leave privacy mode ON (the default).
That way the bot only sees messages that mention it or reply to
its messages — not every chat in the group.
Roles
Four built-in roles (see aipager/safety.py):
| Role | Send prompts | Approve | Bypass deny rules | Bypass the safety floor |
|---|---|---|---|---|
owner | ✅ | ✅ | ✅ | ✅ |
admin | ✅ | ✅ | ✅ | ❌ |
user | ✅ | ✅ | ❌ | ❌ |
read_only | ❌ | ❌ | ❌ | ❌ |
- owner — full control, including the built-in safety floor. There should be exactly one: the person who runs the machine.
- admin — bypasses role deny rules (their Allow tap works on a restricted tool), but the safety floor still applies.
- user — deny rules apply; an Allow tap on a rule-denied tool is auto-rejected.
- read_only — observers. They see every message and can call
/status, but their text / voice / file messages are ignored.
Custom roles can be defined in policy.yaml; the four above are the
defaults it layers on top of.
aipager.yaml schema (team parts)
schema_version: 3
bot_token: "…"
scopes:
- kind: group
chat_id: -100123456789
members:
- id: 12345 # Telegram user ID (NOT a label, NOT a chat ID)
label: alice # how the user is referenced in chat (@alice)
role: owner
- id: 67890
label: bob
role: user
- id: 11111
label: charlie
role: read_only
A DM scope (kind: dm) and a group scope can coexist — the bot then
serves both chats, with sessions namespaced per chat.
How rules work
~/.config/aipager/policy.yaml defines per-role rules. Validate it
with aipager policy validate. Supported fields per role:
deny_tools— tool names auto-denied without prompting (e.g.Write,Edit,Bash,WebFetch).allow_tools— if non-empty, an allow-list: everything else is denied for that role.deny_bash_patterns— patterns matched againstBashinputs.deny_paths_no_access/deny_paths_no_write— path rules.
Underneath all roles sits a built-in safety floor (protected
paths and command patterns) that only owner bypasses.
When claude asks for permission to use a rule-denied tool and the driver's role does not bypass rules:
- The bot does not show the permission prompt.
- It writes a deny back to claude.
- It posts a one-line notice in the chat, e.g.
⛔ [jim] · Auto-denied · Write · (triggered by @bob). - It writes an audit record with
denied: true.
Who a message runs as
Enforcement keys off who sent each message, not whose turn it happens to interrupt:
- Every Telegram message carries its sender's identity and rules into the session (see security → team-mode enforcement). A message held back — because a permission prompt was open, or because someone else's message was still waiting — is delivered later with the original sender's permissions, not whoever drove the session most recently.
- Messages from different users are never merged into one turn: while one user's message is still waiting to be picked up, another user's message is held until it clears. If a mixed turn happens anyway, it runs under the most restrictive combination of the contributors — privileges never widen.
- Buttons that act on a running turn (Stop, Kill, Restart, Replace,
perms-switch) refuse when tapped from a card belonging to an
earlier task —
That task already finished — …— so a stale tap in a busy group can't destroy someone else's current work.
What everyone sees
Permission audits. Every Allow / Deny is replied to in chat:
✅ [jim] · Allowed by @alice · Bash: ls -la /tmp
🚫 [jim] · Denied by @bob · WebFetch: https://example.com
⛔ [jim] · Auto-denied · Edit · (triggered by @bob)
So even if you weren't watching live, scrolling back tells you exactly who decided what.
Message states. 👀 on a message means it was sent to the
session; 👍 means Claude took it; 🤷 means it was dropped before
Claude took it. /whoami shows your own id and role.
The pinned status bar. The group gets its own pinned status bar (see commands → the pinned status bar) listing only this group's sessions, if the bot is an admin allowed to pin. Without that right the bar is skipped for the group: the message is deleted rather than left unpinned in the history.
On-disk audit. ~/.claude/aipager-audit.jsonl records every
decision with user_id, username and scope fields so admins can
post-hoc reconstruct what each user did.
Privacy considerations
- The chat filter still applies. Even with team mode, the bot only listens to the configured chat(s). Adding the bot to a second group doesn't activate it there.
- Read-only users can read prompts and tool inputs. They can't act, but they see everything. If you need to hide some conversations from an observer, that observer doesn't belong in the group.
aipager.yamlandpolicy.yamlare mode 0600 (owner-only).- The audit log is owner-only (
~/.claude/aipager-audit.jsonl).
Revoking a user
- Run
aipager config→ Remove a user (or hand-editaipager.yamland delete their member entry). - The wizard reloads the daemon live (SIGUSR1) — no restart needed.
After a hand-edit, send the signal yourself:
kill -USR1 $(pgrep -f 'aipager start') - Optionally also kick them from the Telegram group.
Step 2 is the security-critical one — until the reload lands, the previous allow-list is still in memory.
Related docs
- Architecture — process model.
- Bot commands — interface reference.
- Security model — trust boundary, threat list.
- Troubleshooting —
aipager doctorreference.