- name
- academic-gate
- version
- 0.9.4
- description
- 学术写作技能的结构签字流程门禁说明(含总开关、强度边界与已知限制)。本目录同时是一个插件——Claude Code 放进 ~/.claude/skills/ 重启一次即自动加载;Codex 读本目录的 .codex-plugin/plugin.json,装完需跑一次 /hooks 信任。PreToolUse 钩子拦截"未确认大纲就写正文"。当用户问门禁为什么拦我、怎么解锁、怎么确认结构签字、门禁没生效怎么办时使用。
# 学术写作结构签字门禁
## 这是什么
8 家学术写作技能(general-sci-writing / review-writing / nsfc-proposal / sci2doc / revise-sci / reviewer-response-sci / reviewer-simulator / polish-sci)共用的一道**流程门禁**:
> **用户没有明确确认大纲/storyline 之前,任何写入正文文件的操作都会被拦下。**
这不是提示词纪律,是 `PreToolUse` 钩子在工具层拦截——并非"提醒 AI 不要跳步",而是工具调用被 CLI 直接拒绝。
## 强度:拦得住什么、拦不住什么
以下明确其能力边界,避免过高或过低的预期。
| 面 | 强度 |
|---|---|
| **AI 用写文件类工具**(Write / Edit / MultiEdit / NotebookEdit / apply_patch)写正文,或改门禁自己的文件 | **完备**。判定按规范化后的绝对路径来;大小写变体、`..`、`~`、相对路径、尾斜杠、双斜杠、空字节、补丁的各种动作头等 18 种写法,实测全部拦下 |
| **AI 经 Bash 命令写** | **拦常见形态,原理上不完备**。shell 里写文件的形态无穷,黑名单堵不完;而且 AI 本来就必须能运行技能自带的脚本,"先改一个不受保护的脚本再运行它"这条路封不掉 |
| **用户自己动手**(编辑器 / 终端里手改) | **完全不拦**。门禁只在 AI 调工具时介入 |
因此定位为:**能拦截无意的遗忘与顺手跳步,无法阻止刻意绕行。** 兜底机制有两项:绕行操作会记入 `<项目根>/.academic_gate_audit.jsonl`,以及最终仍有人工审核。如需彻底禁止跳步,参见下方「彻底锁死」一节。
### 已知限制(有意不修,登记在此)
1. **签字绑定大纲之后,把大纲文件挪走或删掉,检查仍返回"通过"**——不拦。但状态卡会显示「已绑定大纲,但本次读不出大纲文件」,用户看得见。即:**可见但不拦**。
2. **门禁被用户关闭时,安装器打印的恢复说明里含开关字段名与取值写法**,这段 AI 也会读到。有意保留:拿掉它用户就丢了恢复说明,而 AI 知道方法也写不进那个文件(开关在写保护清单里)。属登记在案的残余风险。
## 总开关(用户可关,AI 关不掉)
如需保留技能但关闭拦截,使用此开关即可,不要删除门禁目录(删除后反而会触发技能的旧安装逻辑将其重新写入 `settings.json`)。
**怎么关**:新建 `~/.claude/academic-gate.local.json`:
```json
{ "enforcement_enabled": false, "note": "我自己盯流程" }
```
`note` 可省,只回显给用户看、不进任何喂给模型的文本。
**怎么恢复**:删掉这个文件,或把值改成 `true`。
**关了什么、不关什么**
| | 关闭后 |
|---|---|
| 拦截(deny / ask) | 停 |
| 每轮注入的项目状态卡、待办提醒 | **照常** |
| 技能自己的流程脚本、盲检、签字落盘 | **照常** |
| 技能开工时的自装动作 | 停,且会把 `settings.json` 里的旧钩子条目摘掉 |
**如何查看当前状态**:运行任一技能的开局脚本(`env_preflight.py` / `init_project.py`),关闭状态下会输出一段以「学术门禁的拦截层已被你关闭」开头的说明,包含开关位置、最后修改日期及用户填写的理由。状态卡**不会**显示"当前不拦",该信息不注入模型上下文。
**判定是严格的,坏文件一律等于"开"**:只有 JSON 的 `false` 才算关。文件不存在、JSON 坏了、顶层不是对象、值写成字符串 `"false"`、读不出、是目录或命名管道——全部按"保护开着"处理。带 BOM 的文件能认(不少编辑器默认写 BOM,认不出会让用户以为关了其实没关)。
## 彻底锁死(真想一步都不许跳时)
门禁自身文件已在写保护清单中,AI 通过写文件类工具无法修改。如需将命令行这一缺口也封堵,可追加以下两项措施(均须用户亲手操作,AI 无权执行):
1. **把开关文件设成只读**:
```bash
touch ~/.claude/academic-gate.local.json && chmod 444 ~/.claude/academic-gate.local.json
```
内容留一个 `{}` 即可:保护照常开着,同时这个文件再也写不进去。
2. **用 CLI 自带的路径禁止规则**:Claude Code 的 `permissions.deny` 支持按路径禁 `Write` / `Edit` / `Bash`,把门禁目录与开关文件加进去,就不再依赖我们这层黑名单。具体写法参见 CLI 官方文档,本插件不代为修改 `settings.json`。
## 🔴 为什么做成插件(本目录的存在理由)
**旧做法**:钩子由技能的 Phase 0 调 `install_gate_hook.py` 安装,写进 `~/.claude/settings.json`。
**旧做法的致命缺陷**:安装这一步**依赖 AI 真的去执行**。而这道门禁的作用恰恰是拦住"AI 想跳步"——**最需要它的场景(AI 跳过流程),恰恰是它不会被装上的场景**。这是循环依赖。
**现做法**:本目录带 `.claude-plugin/plugin.json`,Claude Code 启动时把它当 `academic-gate@skills-dir` 插件加载,**钩子由 Claude Code 自己装,全程不经过 AI**。
## 装法
把本目录放进 `~/.claude/skills/`,**重启一次 Claude Code**。验证:
```bash
claude plugin list # 应出现 academic-gate@skills-dir
```
⚠️ 钩子在启动时加载,**无法热生效**——放进去当次会话仍然没有保护,重启后才有。
## 被拦住了怎么办
拦截信息形如:
```
[学术门禁]「structure_signoff」未通过:<项目根> 尚未落盘结构签字
```
**此为流程被跳过的正常拦截,并非 bug。** 正确做法:
1. 回到对应技能的流程,把大纲/storyline 跟用户过一遍
2. **用户明确确认后**(且仅在此之后),运行该技能打印的 `SIGNOFF_CMD` 落盘签字
3. 再继续写正文
🔴 **严禁在用户未确认时自行运行 confirm**,此行为等同于伪造用户签字。
## 门禁没生效怎么办
按顺序排查:
| 现象 | 原因 | 处置 |
|---|---|---|
| `claude plugin list` 里没有 `academic-gate@skills-dir` | 没重启,或目录不在 `~/.claude/skills/` 下 | 重启;确认目录位置 |
| 有插件但不拦 | `gate_registry.json` 里该技能 `signoff: false` | 查 registry,这可能是有意的 |
| Codex 里不拦 | Codex 也有这套钩子(事件名、配置结构、拦截 JSON 都一样),但**要按 Codex 的方式装、并且要你亲手点信任**:它读的是本目录的 `.codex-plugin/plugin.json`;装完必须在 Codex 里跑一次 `/hooks`,把这几条钩子逐条信任,**没信任的钩子会被直接跳过**(不报错) | 装好并信任后,真的学术写作项目(有状态文件的那种)在 Codex 上照拦不误。两件必知的事:<br>① **改过 `hooks/hooks.json` 之后信任会自动作废**——Codex 按钩子定义算一个 hash 记账,定义一变就当没信任过,得再跑一次 `/hooks`(改 `scripts/` 里的 Python 不影响)。<br>② **只是撞了名字的陌生项目,在 Codex 上不拦、只记一条审计**:别人目录里恰好有个 `sections/` 或 `state.json`,但没有我们技能的状态签名,这种情况在 Claude Code 上是弹一句"要不要按学术项目管"让你选,而 **Codex 的拦截框没有"允许一次"**——硬拦会把不相干的项目卡死,唯一出路是去停掉整个插件。所以那一档按放行处理,只在审计里留一条记录。 |
| OpenCode 里不拦 | **OpenCode 只认它自己的 JS 插件,从来不读这套钩子配置** | 已知限制,不是回归:那端只有技能能用,门禁在那边不存在 |
| 拦截理由显示两次 | 插件钩子 + 旧的自装钩子并存 | 无害(两条指向同一份逻辑、不会误放行);跑一次任一技能的 `env_preflight` 会自动摘掉旧的 |
| **Windows 上五个钩子全都不响** | 钩子命令是 POSIX shell 形态(`exec "$(python3 -c '' >/dev/null 2>&1 && command -v python3 \|\| command -v python)" …`),需要 bash 才能跑。**跟你从哪个终端启动 Claude Code 无关**——Claude Code 自己去找 bash:机器上装了 **Git for Windows** 就用它自带的 Git Bash,没装就没法跑 | 装 [Git for Windows](https://git-scm.com/downloads/win)(装完重启 Claude Code 即可,不必从 Git Bash 窗口里启动)。<br>五条 handler 都已显式声明 `"shell": "bash"`:没装 Git Bash 时 Claude Code 会明确报 `Hook "…" requires bash but Git Bash was not found`,而不是悄悄改用 PowerShell 跑一串它读不懂的命令。<br>🔴 但**报错不等于拦截**:官方只把退出码 2 当阻断,钩子起不来一律算"非阻断错误",工具照常执行——所以此时**拦层是哑的**(门禁等于不存在),别把"没被拦"当成"检查通过了"。这条报错每个会话每条钩子只提示一次,错过就没了。<br>**开局没看到学术项目状态卡 = 钩子没在岗**——这是最快的自检:卡片首行会带 `academic-gate v<版本号>`。<br>TODO:仍未做真正的跨壳改造(命令还是 bash 形态,PowerShell 上依然跑不了)。本轮只把"静默退化"换成了"一次性明确报错"。彻底解法是改 exec 形态(`command` + `args`,不经壳),改动面涉及全部 5 条 handler,需单独验证 |
## 目录内容
```
academic-gate/
├── .claude-plugin/plugin.json 插件声明(name 写死,不靠目录名推导)
├── .codex-plugin/plugin.json 同上,给 Codex 用(两份逐字节一致,改一份要一起改)
├── hooks/hooks.json 5 条 handler:喂 3 个事件 + 拦 Write 类 + 拦 Bash
├── scripts/
│ ├── academic_gate_hook.py 【拦·写文件】PreToolUse(Write|Edit|MultiEdit|NotebookEdit)。
│ │ 判是不是学术项目 → 伪造签字/盲检凭证、未补盲检就写新正文、
│ │ 未结构签字 → deny;认不出项目一律放行(fail-open)
│ ├── bash_guard_hook.py 【拦·Bash】PreToolUse(Bash)。**会 deny 你的 Bash 命令**:
│ │ AI 代跑 structure_signoff_gate.py confirm、用 shell 绕开
│ │ 脚本直写 structure_signoff.json / .review_pass/、经 shell
│ │ 写未过盲检的受管正文。黑名单、拦常见形态,不完备
│ ├── context_feed_hook.py 【喂】SessionStart / UserPromptSubmit / PostToolUse。
│ │ **每轮会往 AI 上下文注入一段文本**(项目状态卡:项目根、
│ │ 技能、哪些节声明完成但没盲检、盲检命令)。非学术项目输出空、
│ │ 全绿时也不注入;只读项目文件,不写你的项目目录
│ ├── context_guard_core.py 三个钩子共用的唯一判定实现(分档/差集/清洗/审计)+
│ │ 排障 CLI:python3 context_guard_core.py explain <路径>
│ ├── structure_signoff_gate.py 签字落盘/校验
│ └── gate_registry.json 各技能的受管文件范围与 signoff 开关
└── SKILL.md 本文件
```
**本插件的写入范围**(仅以下三处):`<项目根>/.academic_gate_audit.jsonl`(只记
deny/ask、Codex 上按放行处理的那一档、与"这次没检查成",同目录已有 `.gitignore` 时追加一行忽略、没有则不创建)、
`${CLAUDE_PLUGIN_DATA}` 下的去重/待报小文件、插件自己目录里的心跳。**不联网、不读密钥。**
陌生目录的边界如实说:撞了通用产物名(如 `drafts/section_*.md`)的目录**零残留**;但写到
我们的凭证名(`.review_pass/` 或 `structure_signoff.json`)时会在那个目录留一行审计。
上面 5 个 `.py` 与 `gate_registry.json` 跟 `_shared/` 逐字节一致,由 `sync_vendored.py` 守卫。**修改任一份均须走同步流程**,禁止单独修改此处的副本。
View on GitHub