设置与权限
设置的分层以及它们如何合并、五种权限模式、规则语法,以及 shell 钩子。
分层
设置是 JSONC——注释和尾逗号都没问题——而且它们会叠起来。优先级从低到高 (ADR-0003):
~/.bingo/settings.json—— 你的,到处生效<cwd>/.bingo/settings.json—— 项目的,会提交<cwd>/.bingo/settings.local.json—— 项目里只属于你的--settings <path>—— 压在上面三层之上的又一个文件- 命令行 flag,作为一个合成出来的最上层
文件不存在就跳过。根不是对象的文件是一个错误,而不是耸耸肩。
键怎么合并
内核拥有五个顶层键——provider、model、thinking、maxTokens、models。其余每个
键都属于认领它的那个插件,而认领的同时也带上了它合并时所依的规则:
| 规则 | 行为 |
|---|---|
Replace | 高层胜出(默认) |
Accumulate | 列表拼接,最低层在前,重复项保留第一份 |
ByName | 以 name 或 id 为键的对象列表和映射;高层的条目就地替换低层的 |
对象在每一层深度上逐字段合并;规则作用在叶子上。高层里一个显式的 null 会清掉它
下面每一层的值,你就是这样收回低层说过的话。
没人认领的键不会被悄悄忽略:bingo 会在启动时报出一条 UNKNOWN_SETTING 通知,并指明
是哪一层设了它,所以 permisions 这样的拼写第一次运行就被逮住。
插件认领的键
| 键 | 归属 | 合并 |
|---|---|---|
permissions.defaultMode | permissions | replace |
permissions.allow / .deny / .ask | permissions | accumulate |
permissions.additionalDirectories | permissions | accumulate |
hooks.<Event> | shell 钩子 | accumulate |
mcpServers | mcp | by name |
disabledMcpServers | mcp | accumulate |
anthropic、openai、codex | 各提供方插件 | replace |
context.memory | context | replace |
web.search、web.braveApiKey | web 工具 | replace |
channels | channels | by name |
plugins | 跨进程桥 | by name |
demoUi | demo 插件 | replace |
插件为自己那一片定类型,所以加一个键既不需要改内核,也没有一个合并函数要跟着同步。
权限模式
模式说的是没有规则做出决定时会发生什么。
| 模式 | 没有规则做决定时 |
|---|---|
default | 受信任的只读工具直接跑;其余一律询问 |
acceptEdits | 工作目录内的编辑不经提问就跑 |
plan | 任何不是只读的东西都根本不跑 |
bypassPermissions | 除了只有人才能决定的那些,其余全跑 |
dontAsk | 没有人在场回答,所以本该询问的一律否决 |
用 --permission-mode <mode> 为一次运行设定,用 /permission <mode> 为一次会话设定,
或者用 permissions.defaultMode 作为下限。在终端界面里 shift+tab 轮换它们,而模式
就是状态行的左侧位置。--dangerously-skip-permissions 恰好等于
--permission-mode bypassPermissions。
用 /permission 选的模式只活在那次会话的内存里,绝不会写进文件。
规则
三张表——allow、deny、ask——拿去匹配工具声明它将要碰的东西。一行一条规则:
{
"permissions": {
"allow": ["Bash(git status:*)", "Read(/src/**)"],
"deny": ["Bash(rm:*)", "WebFetch(domain:internal.example.com)"],
"ask": ["Edit"]
}
}
这套语法:
| 写法 | 匹配 |
|---|---|
Tool | 该工具的每一次调用 |
Tool(*) | 同上——什么都不指名的规则也就什么都没收窄 |
Tool(text) | 一条命令、一个路径或一个 url 的前缀;Name 类匹配对象的精确名字 |
Tool(text:*) | 把 text 当前缀,在每一种匹配对象上 |
Tool(prefix:text) | 同上,去掉 prefix: |
Tool(/src/**) | 一个路径 glob,其中 * 不跨越分隔符 |
Tool(domain:host) | url 的主机名,精确匹配 |
mcp__server | 该服务器的每一个工具 |
mcp__server__tool | 那一个工具 |
plugin__name__tool | 来自某个跨进程插件的工具 |
deny 和 ask 按宽的方式读一条规则——命中一处就够了。allow 按窄的方式读:每一个
匹配对象、以及一行 shell 里的每一条子命令,都必须被覆盖到。每张表都取那个失败即拒绝
的读法。
--allowed-tools 'Bash(git status:*)' 为一次运行加上 allow 规则。它可以重复给出,
而一个 flag 也接受逗号分隔的列表。
一个工具被信任到什么程度
工具会声明自己是不是只读、和别的工具并排跑安不安全,以及一次打断该对它做什么。
未知的工具失败即拒绝:bingo 不认识的工具既不是并发安全的,也不是只读的,而它的
打断行为是阻塞。所有从二进制之外到达 bingo 的东西——一个 MCP 服务器的工具、一个跨进程
插件的工具——统统穿着这套默认值,不管它自己怎么说。readOnlyHint 是一句声称,从来
不是一个事实。
钩子
bingo-hooks-shell 在 bingo 的生命周期点上运行你自己的命令,遵循 Claude Code 的钩子
契约:事件作为 JSON 从 stdin 进来,裁决作为 JSON 从 stdout 出去,而退出码决定一切——
0 是“以下是我要说的话”,2 拦下并以钩子自己的话作为理由,其他任何值都是一个坏掉的
钩子,它根本轮不到做决定。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "./scripts/guard.sh" }]
}
]
}
}
有十个事件背后确实有一个 bingo 的生命周期点:PreToolUse、PostToolUse、
PostToolUseFailure、PermissionRequest、UserPromptSubmit、Stop、PreCompact、
SessionStart、SessionEnd、Notification。这份名单之外的事件名会导致启动失败——
一个没人会去跑的钩子,是它作者以为已经生效的一条规则。这里只存在 type: "command"
的钩子;别的类型会在启动时被拒绝,而不是被默默跳过。
有两处偏离值得知道:
- 超时是 60 秒,
SessionEnd是 1.5 秒。单个钩子的timeout覆盖这个值,但SessionEnd仍然被封顶。 permissionDecision: "allow"并不跳过闸门。 bingo 只有一条权限路径——那就是 策略——而钩子不是它。allow读作“没有异议”,这次调用照样送去闸门。钩子能收紧发生 的事;它永远放不宽。
SessionStart 钩子可以往 BINGO_ENV_FILE 指向的路径里追加 KEY=value 行,那次会话
里后面的每一个钩子都会带着它们运行。这个文件是按赋值来读的,不是当 shell 来 source。
钩子也可以来自一个跨进程插件,走的是类型化的 schema 和一条长连接,而不是每个事件 fork 一次——见跨进程插件。