copilot-bridge is configured via a JSON file. The bridge checks these locations in order:
$COPILOT_BRIDGE_CONFIGenvironment variable~/.copilot-bridge/config.json./config.json(current working directory)
See config.sample.json for the full format.
The bridge watches config.json for changes and hot-applies safe settings automatically — no restart needed for most edits.
Hot-reloadable (applied immediately):
- Channel settings:
triggerMode,threadedReplies,verbose,model,agent - Defaults:
model,agent,triggerMode,threadedReplies,verbose,permissionMode - Permissions:
allow,deny,allowPaths,allowUrls - Bot config:
agent,adminflag,access(user allowlist/blocklist) - Platform-level
access(user allowlist/blocklist) - New channel entries
Restart required (config updates but adapters keep old values):
- Platform
urlchanges (adapter caches URL at construction) - Bot
tokenchanges (adapter caches token at construction) - Adding/removing platforms or bots (needs new adapter + WebSocket connection)
On reload failure (invalid JSON, validation errors), the existing config is preserved. The bridge logs what changed and warns about restart-needed fields.
Manual reload: Use /reload config to trigger a reload without waiting for the file watcher. This shows exactly what changed.
Only platforms defined in this section are loaded — if a platform isn't listed here, the bridge won't attempt to connect to it, even if the corresponding adapter package is installed.
Define your messaging platform connections and bot identities:
{
"platforms": {
"mattermost": {
"url": "https://chat.example.com",
"bots": {
"copilot": { "token": "BOT_TOKEN_1", "admin": true },
"alice": { "token": "BOT_TOKEN_2", "agent": "alice-agent" }
}
}
}
}Each bot needs a platform bot account and token. Set "admin": true on the bot that should manage the bridge — admin bots get extra tools (grant_path_access, workspace management, config editing) and use the admin AGENTS.md template. This is a bridge-level setting, not a platform permission. If you only have one bot, make it admin.
If you only need one bot, use botToken instead of bots:
{
"platforms": {
"mattermost": {
"url": "https://chat.example.com",
"botToken": "SINGLE_BOT_TOKEN"
}
}
}Control which users can interact with bots using the access block. Access can be set at platform level (inherited by all bots) and/or bot level (grants additional users for that bot).
Allowlists are additive — a user is allowed if they appear in the platform allowlist OR the bot allowlist. Platform users inherit access to all bots; bot-level users only get access to that specific bot. Blocklists always win — a blocked user is denied regardless of allowlists at either level.
{
"platforms": {
"mattermost": {
"url": "https://chat.example.com",
"access": {
"mode": "allowlist",
"users": ["chris"]
},
"bots": {
"copilot": {
"token": "BOT_TOKEN"
},
"alice": {
"token": "BOT_TOKEN_2",
"access": {
"mode": "allowlist",
"users": ["alex"]
}
},
"bob": {
"token": "BOT_TOKEN_3"
}
}
}
}
}In this example:
- chris can talk to all bots (platform allowlist inherited everywhere)
- alex can only talk to alice (bot-level allowlist on alice)
- bob bot: no bot-level access, so only platform users (chris) can use it
| Mode | Behavior | Default? |
|---|---|---|
"allowlist" |
Only listed users can use the bot | — |
"blocklist" |
Everyone except listed users | — |
"open" |
All users can use the bot | — |
Access is additive across levels:
- Neither level configured → deny all (secure by default)
- Only platform configured → platform decides alone
- Only bot configured → bot decides alone
- Both configured (allowlists) → user passes if listed at either level (union)
- Blocklist at any level → always denies matched users, regardless of allowlists
This means platform allowlist users inherit access to every bot, while bot-level allowlists can grant additional users for specific bots.
⚠️ Breaking change (v0.8.0): Previously, missingaccessdefaulted to open. Now, if noaccessblock exists at either level, all users are denied. Add"access": { "mode": "open" }at the platform or bot level to restore the previous behavior, or runcopilot-bridge initto configure access during setup.
- Mattermost: Use the Mattermost username (handle). Case-insensitive. Leading
@is stripped automatically. - Slack: Use the Slack user ID (e.g.,
U12345ABC). Duringinit, you can enter a Slack handle and it will be resolved to a UID via the Slack API. Handles manually added to the config are resolved to UIDs on startup.
The access check matches each entry against both the user's platform ID and username, so either format works.
Note: Bot-level access control requires the
botsconfig format (not thebotTokenshorthand). Platform-level access works with both formats.
Messages from unauthorized users are silently dropped — no response is sent. Drops are logged at DEBUG level for troubleshooting.
Access config changes are hot-reloadable — edits take effect after /reload config or automatic file watcher pickup, with no restart needed.
Channel entries map a specific Mattermost channel to a bot, working directory, and preferences:
{
"channels": [
{
"id": "channel-id-from-mattermost",
"platform": "mattermost",
"bot": "copilot",
"name": "My Project",
"workingDirectory": "/path/to/project",
"model": "claude-sonnet-4.6",
"triggerMode": "mention",
"threadedReplies": true,
"verbose": false
}
]
}| Field | Required | Description |
|---|---|---|
id |
Yes | Mattermost channel ID |
platform |
Yes | Platform name (e.g., mattermost) |
bot |
Yes | Bot identity to use |
name |
No | Human-readable label (for logs) |
workingDirectory |
Yes | Local path for the Copilot session |
model |
No | AI model override |
fallbackModels |
No | Ordered list of fallback models if the primary is unavailable |
agent |
No | Custom agent name |
triggerMode |
No | "mention" (default) or "all" |
threadedReplies |
No | Use threaded replies (default: true) |
verbose |
No | Show tool calls (default: false) |
You don't need one for DMs. The bridge auto-discovers DM channels for each bot at startup. DMs default to triggerMode: "all", threadedReplies: false, and the bot's default workspace (~/.copilot-bridge/workspaces/<botname>/).
You need a channel entry when:
- You want to point a DM at a custom
workingDirectory(overriding the default workspace) - You're using a group/team channel (non-DM)
- You want
triggerMode: "mention"so the bot only responds when @-mentioned
Fallback values for any setting not specified per-channel:
{
"defaults": {
"model": "claude-sonnet-4.6",
"fallbackModels": ["claude-sonnet-4.5"],
"agent": null,
"triggerMode": "mention",
"threadedReplies": true,
"verbose": false,
"permissionMode": "interactive"
}
}Config-level permission rules use Copilot CLI-compatible syntax:
{
"permissions": {
"allow": [
"read",
"shell(ls)",
"shell(cat)",
"vault-search",
"context7"
],
"deny": [
"shell(rm)",
"shell(git push)"
],
"allowPaths": [],
"allowUrls": [
"docs.github.com",
"stackoverflow.com"
]
}
}| Pattern | Matches |
|---|---|
"read" |
All file reads |
"write" |
All file writes |
"shell" |
All shell commands |
"shell(ls)" |
The ls command |
"shell(git push)" |
git push (any args) |
"shell(open -a Obsidian)" |
Commands starting with open -a Obsidian |
"vault-search" |
All tools from that MCP server |
"vault-search(search)" |
Only the search tool from that server |
- Hardcoded safety denies — blocks destructive commands (
rm -rf /,mkfs, fork bombs, etc.) even in autopilot mode. Cannot be overridden. - Autopilot mode — if enabled, auto-approves everything else (skip steps 3–5)
- Config deny rules — checked first among config rules, always wins
- Config allow rules — if matched, auto-approved
- SQLite stored rules — from
/always approveor/always denyin chat (MCP rules save at server level) - Interactive prompt — asks the user in chat with approve/deny reactions
Use /yolo in chat to auto-approve all permissions (hardcoded safety denies still apply). Use /autopilot to enable the SDK's autonomous agentic loop — the agent works continuously until the task is done. Use /plan to toggle plan mode for structured planning before implementation. These are independent: /yolo controls permissions, /autopilot and /plan control agent behavior.
Use /always approve or /always deny during a permission prompt to persist the rule. Use /rules to see all permission rules (hardcoded, config, and stored).
The ask_agent tool allows bots to communicate with each other. When enabled, any agent can ask another agent a question by creating a fresh ephemeral session for the target bot.
Disabled by default. Add interAgent to your config to enable:
{
"interAgent": {
"enabled": true,
"defaultTimeout": 60,
"maxTimeout": 300,
"maxDepth": 3,
"allow": {
"max": { "canCall": ["alice"], "canBeCalledBy": ["alice"] },
"alice": { "canCall": ["max"], "canBeCalledBy": ["max"] },
"summarizer": { "canCall": [], "canBeCalledBy": ["*"] }
}
}
}| Field | Default | Description |
|---|---|---|
enabled |
false |
Master switch for inter-agent communication |
defaultTimeout |
60 |
Default timeout in seconds for ephemeral calls |
maxTimeout |
300 |
Maximum timeout the calling agent can request |
maxDepth |
3 |
Maximum call chain depth (A→B→C = depth 2) |
allow |
— | Per-bot allowlist: canCall and canBeCalledBy arrays. Use "*" for any bot. |
- Agent Max calls
ask_agent({ target: "alice", message: "What's the thermostat set to?" }) - The bridge validates the allowlist (Max can call Alice, Alice accepts calls from Max)
- A fresh ephemeral session is created using Alice's workspace, AGENTS.md, MCP servers, and skills
- The target session receives a system prompt listing all of Alice's project workspaces (workspace awareness)
- Alice's session processes the message and returns the response as tool output to Max
- The ephemeral session is torn down
| Parameter | Required | Description |
|---|---|---|
target |
✅ | Bot name to ask (e.g., "alice") |
message |
✅ | The question or request |
agent |
Specific *.agent.md persona in the target's agents/ directory |
|
timeout |
Timeout in seconds (capped at maxTimeout) |
|
autopilot |
Auto-approve permissions in the ephemeral session (default: false) |
|
denyTools |
Tools to deny in the ephemeral session (e.g., ["bash"]) |
|
grantTools |
Tools to pre-approve (only if the caller also has them approved) |
Place *.agent.md files in a bot's workspace agents/ directory:
~/.copilot-bridge/workspaces/alice/agents/network.agent.md
~/.copilot-bridge/workspaces/alice/agents/hvac.agent.md
Call with ask_agent({ target: "alice", agent: "network", message: "..." }) to activate a specific persona.
When a bot serves multiple channels with different working directories, the ephemeral session automatically receives a workspace map listing all of the target bot's projects. The target bot can reason about which project is relevant to the question — no channel parameter needed.
Three layers prevent infinite loops:
- Visited set — A bot cannot appear twice in the same call chain (catches A→B→A immediately)
- Depth cap — Hard limit on call chain length (default: 3)
- Config allowlist — Only explicitly permitted call paths are allowed
Ephemeral sessions use merged permissions: the target bot's own rules plus the caller's approved permissions as supplementary grants. Hardcoded safety denies always apply. If autopilot: false (default) and a permission can't be resolved, the call returns an error to the caller with detail about what was blocked.
All inter-agent calls are logged to SQLite (agent_calls table) with caller, target, duration, success/failure, and call chain metadata.
Session hooks let you run shell commands at key lifecycle points — before/after tool calls, on user prompts, session start/end, and errors. Hooks use the official GitHub Copilot CLI hooks format.
{
"version": 1,
"hooks": {
"preToolUse": [
{
"type": "command",
"bash": "./scripts/guard.sh",
"cwd": ".",
"timeoutSec": 10
}
]
}
}Each hook is an array of command objects. Multiple commands per hook type run in sequence.
| Field | Required | Description |
|---|---|---|
type |
Yes | Must be "command" |
bash |
Yes* | Shell command to run (use powershell on Windows) |
cwd |
No | Working directory, relative to the hooks.json location |
timeoutSec |
No | Max execution time (default: 30s) |
env |
No | Extra environment variables |
Hooks receive JSON on stdin describing the event (tool name, arguments, etc.) and return JSON on stdout. For preToolUse, the output controls whether the tool runs:
{ "permissionDecision": "allow" }{ "permissionDecision": "deny", "permissionDecisionReason": "Blocked by policy" }{ "permissionDecision": "ask", "permissionDecisionReason": "Confirm before running" }allow— tool proceeds normally (this is also the default if the hook returns nothing)deny— tool is blocked; the reason is shown to the agentask— the bridge prompts the user for approval in chat (approve/deny only; "always" and "remember" are not offered for hook-triggered prompts)
For preToolUse, if multiple commands are registered, the first "deny" or "ask" short-circuits (precedence: deny > ask > allow).
#!/bin/bash
# guard-main-push.sh — Block git push to main branch
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.toolName // empty')
COMMAND=$(echo "$INPUT" | jq -r '.toolArgs.command // empty')
if [ "$TOOL" = "bash" ] && echo "$COMMAND" | grep -q "git push.*main"; then
echo '{"permissionDecision":"deny","permissionDecisionReason":"Push to main blocked by hook"}'
else
echo '{"permissionDecision":"allow"}'
fi| Hook Type | When it Fires |
|---|---|
preToolUse |
Before tool execution (can allow/deny/ask) |
postToolUse |
After tool execution |
userPromptSubmitted |
When user sends a message |
sessionStart |
Session created or resumed |
sessionEnd |
Session ends |
errorOccurred |
Error occurs |
Hooks are loaded from multiple locations (lowest to highest priority). Commands from all sources are appended, not overridden:
- Plugin hooks —
~/.copilot/installed-plugins/.../hooks.json - User hooks —
~/.copilot/hooks.json - Workspace hooks —
<workspace>/.github/hooks/hooks.json,<workspace>/.github/hooks.json, or<workspace>/hooks.json(disabled by default)
Security note: Workspace hooks execute arbitrary code. They are disabled by default to prevent untrusted repositories from running code automatically. To enable, set
"allowWorkspaceHooks": truein thedefaultssection of your bridge config.
Use /tools in chat to see which hooks are currently loaded and how many commands are registered per hook type.
By default, the bridge uses SQLite at ~/.copilot-bridge/state.db. The state layer is pluggable — you can swap in a custom backend (Postgres, Redis, etc.) by pointing database.module at a module that exports a class implementing the StateStore interface.
{
"database": {
"module": "./path/to/custom-store.js",
"options": { "connectionString": "postgresql://..." }
}
}module— Path to a JS module. Must export a class (default export, namedStateStoreexport, or the module itself) that implements theStateStoreinterface. See StateStore interface for the contract.options— Arbitrary object passed to the custom store's constructor.
When database is omitted (the default), the built-in SqliteStateStore is used with no additional configuration.