跳至正文
浏览章节

设置与权限

设置的分层以及它们如何合并、五种权限模式、规则语法,以及 shell 钩子。

分层

设置是 JSONC——注释和尾逗号都没问题——而且它们会叠起来。优先级从低到高 (ADR-0003):

  1. ~/.bingo/settings.json —— 你的,到处生效
  2. <cwd>/.bingo/settings.json —— 项目的,会提交
  3. <cwd>/.bingo/settings.local.json —— 项目里只属于你的
  4. --settings <path> —— 压在上面三层之上的又一个文件
  5. 命令行 flag,作为一个合成出来的最上层

文件不存在就跳过。根不是对象的文件是一个错误,而不是耸耸肩。

键怎么合并

内核拥有五个顶层键——provider、model、thinking、maxTokens、models。其余每个 键都属于认领它的那个插件,而认领的同时也带上了它合并时所依的规则:

规则行为
Replace高层胜出(默认)
Accumulate列表拼接,最低层在前,重复项保留第一份
ByName以 name 或 id 为键的对象列表和映射;高层的条目就地替换低层的

对象在每一层深度上逐字段合并;规则作用在叶子上。高层里一个显式的 null 会清掉它 下面每一层的值,你就是这样收回低层说过的话。

没人认领的键不会被悄悄忽略:bingo 会在启动时报出一条 UNKNOWN_SETTING 通知,并指明 是哪一层设了它,所以 permisions 这样的拼写第一次运行就被逮住。

插件认领的键

键归属合并
permissions.defaultModepermissionsreplace
permissions.allow / .deny / .askpermissionsaccumulate
permissions.additionalDirectoriespermissionsaccumulate
hooks.<Event>shell 钩子accumulate
mcpServersmcpby name
disabledMcpServersmcpaccumulate
anthropic、openai、codex各提供方插件replace
context.memorycontextreplace
web.search、web.braveApiKeyweb 工具replace
channelschannelsby name
plugins跨进程桥by name
demoUidemo 插件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 一次——见跨进程插件。