kishormorol/nightaudit
GitHub: kishormorol/nightaudit
一款利用闲置 AI CLI 订阅在夜间自动执行只读代码审查并生成每日摘要的自动化工具。
Stars: 25 | Forks: 1
# nightaudit
**审计并不会改变账本。**
让你闲置的 Claude Code 或 Codex 订阅发挥作用——在你忙碌时对你的项目进行只读审查,每天早晨提供一份摘要。

[](https://pypi.org/project/nightshift-cli/)
[](LICENSE)
[](pyproject.toml)
## 这是什么
你已经在为 Claude Code 或 ChatGPT 付费。那个订阅在一天中的大部分时间,且毫无疑问在整个晚上,都处于闲置状态。nightaudit 利用这些闲置时间审查你指定的项目,并将结果留在一份 Markdown 文件中,让你在喝咖啡时阅读。
两种 CLI 都可以——Claude Code 或 Codex,取决于你拥有哪一个。如果两者都有,它会同时使用它们,且各自有独立的预算。
它绝不编辑你的代码。它没有 daemon 也没有 server——由 cron 调用它,它自行决定是否运行,然后继续休眠。它所知道的一切都存在于两个你可以随时删除的目录中。
这就是该工具的全部。本页面的其余部分是关于如何使用它。
## 开始之前
- **Python 3.10+**
- **至少一个 AI CLI**,已安装并已登录——可以是
[Claude Code](https://claude.com/claude-code) 或
[Codex](https://developers.openai.com/codex)。运行 `claude --version` 或
`codex --version`;如果该命令有效,nightaudit 就能找到它。如果两者都有,它会同时使用它们,且各自有独立的预算。
- **cron**——macOS 和 Linux 上的标准配置。Windows 可通过 WSL 使用。
你不需要 API key。nightaudit 驱动着你手动使用的相同 CLI,使用的是你已有的订阅。
## 安装
```
pipx install nightshift-cli
nightaudit --version
```
这不是拼写错误。除了安装行之外,该工具在任何地方都叫 `nightaudit`:在改名之前,它以 `nightshift-cli` 的名称发布,而 PyPI 无法在不抛弃所有使用旧名称用户的情况下重命名项目。因此,包保留了旧名称,而命令获得了新名称。你今天只需输入一次 `nightshift-cli`,以后再也不用了。
(`uv tool install nightshift-cli` 和 `pip install nightshift-cli` 也可以;
pipx 和 uv 只是让它不干扰你的其他环境。无论哪种方式,命令都是
`nightaudit`。)
**从 0.3.0 版本升级(当时它被称为 nightshift)?** `pipx upgrade
nightshift-cli` 然后继续使用。你的配置、预算历史和队列会在它们原本所在的 `~/.nightshift` 中被读取,旧的 `nightshift` 命令仍然有效,因此你现有的 crontab 会继续运行——它会打印一条通知并完成任务。但这并非永久有效:在方便时运行 `nightaudit init`,它会就地重写旧的 cron 块。该别名将在 1.0 版本中移除。要随时移动状态,可以使用 `mv ~/.nightshift ~/.nightaudit`——它会在下次运行时被自动识别。
## 设置
运行一次 `nightaudit init`。它会查找你的 AI CLI,询问要审查哪些项目以及何时审查,写入配置文件,并提议安装驱动一切的 cron 行:

项目路径是它唯一需要你提供的内容。其他所有内容都有可以通过按回车键(Enter)接受的默认值:
| 它询问的内容 | 含义 | 默认值 |
| --- | --- | --- |
| **project path** | 要审查的 repo。你可以根据需要输入任意多个;空行表示结束。 | — |
| **name** | 在摘要中如何称呼它。 | 目录的名称 |
| **tasks** | 要对它运行哪些审查。 | `code_review, security_audit, deps_audit` |
| **windows** | 允许运行的时间段,本地时间。 | `00:00-06:00` |
| **idle minutes** | 你必须首先离开 Claude Code 多长时间。 | `60` |
| **digest dir** | 早晨摘要存放的位置。 | `~/nightaudit-reports` |
它写入的所有内容都会进入 `~/.nightaudit/config.yaml`。你可以随时手动编辑它——它是纯 YAML,并且 `nightaudit status` 会对它进行验证。
## 查看运行情况
不要等到今晚。现在就强制运行一次:
```
nightaudit run --now
```
`--now` 会跳过窗口和闲置检查,让你立即获得审查结果。在终端中,nightaudit 会实时流式传输——你会看到与你亲自运行 `claude` 时相同的读取和推理过程:

那是 nightaudit 针对其自身仓库的一次真实运行,而那些都是真实的 bug。其中两个在同一天晚上变成了提交:一个可能导致永远挂起并持有调度器锁的运行 (`claude_code.py:366`),以及一个可能被错误所有者释放的锁 (`lock.py:121`)。
发现按 🔴 HIGH、🟠 MED、🟡 LOW 排序,每个发现都引用了 `file:line`,以便你可以直接跳转到那里。
## 然后忘了它
如果你让 `init` 安装了 cron 行,你就已经完成了。Cron 每小时调用一次
`nightaudit run`;该命令会自行决定是否采取行动,并在答案为“否”时静默退出:
```
0 * * * * ~/.local/bin/nightaudit run >> /tmp/nightaudit-cron.log 2>&1
30 7 * * * ~/.local/bin/nightaudit digest >> /tmp/nightaudit-cron.log 2>&1
```
每小时运行的行是受限的;07:30 的行用于渲染昨天的发现。
`init` 会写入二进制文件的绝对路径,因为 cron 运行时的 `PATH` 极有可能不包含你的路径,并且会将两个流重定向到日志中,因为 cron 的输出无处可去。
只有当**所有四个门**都打开时,才会发生运行:
1. **Window**——时钟位于你 `schedule.windows` 的其中一个时间段内。
2. **Idle**——你已经有一段时间(`idle_minutes`)没有触碰 Claude Code 了。nightaudit 会监视
`~/.claude/projects` 并在你工作时避开你。
3. **Budget**——你今天和本周还有剩余的运行次数。
4. **Lock**——没有其他运行正在进行中。
任何一个门说“不”都是正常的,而不是错误。它会打印一行并以 exit 0 退出:
```
$ nightaudit run
nothing to do — outside configured windows (00:00-06:00)
```
每次运行都会从一个持久化的轮询队列中弹出一个 `(project, task)` 对,
因此每个项目都会轮到,而一个嘈杂的项目不会让其他项目挨饿。
要随时检查它的状态,请问:

并且要查看由 cron 启动的运行——包括已经在进行中的运行——
`nightaudit watch` 会实时跟随,并首先重放上次完成的运行。
## 阅读摘要
每次运行都会追加到 `~/nightaudit-reports/YYYY-MM-DD/`。每天一次,
`nightaudit digest` 会将它们渲染成一个文件,`DIGEST-YYYY-MM-DD.md`:
```
# Nightaudit · 晨间摘要
Wed Jul 15, 2026 · generated 17:57 local · 1 project · 1 run
## 剩余预算
- `claude_code` ▓░░░░░ 1/6 today · 1/30 week
## 重点
- 🔴 Replace the buffered `subprocess.run(..., timeout=...)` with the same `Popen` +
`os.killpg` treatment the streaming path uses … — _nightaudit · code_review_ ·
`nightaudit/adapters/claude_code.py:267`
- 🟠 Have `release()` re-read the lockfile and unlink only when the recorded pid is
still our own … — _nightaudit · code_review_ · `nightaudit/lock.py:121`
## 运行日志
| project | task | provider | status | dur | time |
| --- | --- | --- | --- | --- | --- |
| nightaudit | code_review | claude_code | ok | 2m18s | 15:23 |
```
最高严重程度的排在前面,按项目分组,二十秒内即可读完。
**[这里是该摘要的完整内容](docs/sample-digest.md)**——是真实的,而不是
模拟的。
跳过和失败的运行会保留在日志中。一次没有发生的运行也是信息,静默丢弃它会让你对该工具失去信任。
想提前获取,或者获取特定某一天的?
```
nightaudit digest # today, written to the digest dir
nightaudit digest --date 2026-07-14 # a past day
nightaudit digest --stdout # print it instead of writing it
```
## AI 触碰 0 个文件
nightaudit 的全部价值取决于能否安全地无人值守运行,因此
在这里只读不仅仅是文档中的承诺——它是在实际执行工具的层面上被强制执行的。
本节是关于 AI 的,标题是故意这么说的。如果你配置了
[checks](#checks),那些是你自己的命令,它们以你的身份运行——请参阅
下文。
Claude Code 适配器使用其自身的权限标志调用 CLI:
```
claude --print "
"
--output-format json
--allowed-tools Read Grep Glob NotebookRead
--disallowed-tools Bash Edit MultiEdit Write NotebookEdit WebFetch WebSearch Task
```
因此,有三件事是真实的:
- **白名单是整个工具的预算。** Claude Code 无法调用不在列表中的工具。
没有 Edit,没有 Write,没有 shell。
- **黑名单是双重保险。** 它的存在是为了防止未来的 CLI 版本向默认值添加新的具备写入能力的工具,从而悄无声息地扩大 nightaudit 的能力范围。
- **没有后备方案。** 如果标志被拒绝,运行就会失败并被记录为失败。
nightaudit 从不重试不受限制的调用。
提示词也强化了这一点,但提示词不是安全边界,也不被视为安全边界。
标志才是。
nightaudit 也绝不触碰你的 git 状态:没有提交,没有分支,没有
推送。顺其自然的话,它只会读取,并且只在一个地方写入——摘要目录。
## 检查
一个 task 是给一个无法写入的 AI 的提示词。一个 **check** 是你自己的命令,nightaudit 会运行它:
```
projects:
- name: gradagent
tasks: [code_review]
checks:
- name: tests
run: pytest -q
- name: lint
run: ruff check .
timeout_s: 30
```
每一个都会在审查之前在项目目录中运行,并连同其退出码和输出的末尾部分进入摘要:
```
### gradagent
#### 检查
- ✗ `tests` — `pytest -q` · exit 1
```
3 个失败,128 个通过
```
- ✓ `lint` — `ruff check .` · exit 0
```
**检查是在沙箱之外的,这正是它的意义所在。** AI 被它无法辩驳的标志限制为只读。你的检查是你的命令,以你的权限运行:`pytest` 会写入 `.pytest_cache/` 是因为你让它这么做的。nightaudit 不对它进行沙箱化,也不假装这么做。
在添加之前,有两件事值得了解:
- **没有 shell。** 命令被拆分为参数并直接运行,因此 `&&`、管道、`*` 和 `$(...)` 不会被解释——`run: rm -rf $HOME` 会将四个字符 `$HOME` 传递给 `rm`。如果你想要一个管道,可以将检查指向一个脚本。
- **`config.yaml` 现在可以执行命令了。** 在检查功能出现之前,它是惰性数据;任何能写入它的东西现在都可以从 cron 以你的身份运行命令。这是让配置文件运行测试的固有特性。`~/.nightaudit/prompts/` 从未获得这种特性——提示词仅仅是交给模型的文本。
失败、超时或指定了未安装程序的检查会被报告,仅此而已。它绝不会导致审查失败——审查才是你正在等待的东西。
## 不要让它烧光你的配额
nightaudit 在你已付费的订阅上运行,这意味着它成为问题的最快方式就是烧光你的配额。因此,它会进行统计。
```
providers:
claude_code:
enabled: true
budget:
max_runs_per_day: 6
max_runs_per_week: 30
```
- **每一次尝试都算数**——包括失败和超时。它们消耗了你的配额,因此它们会消耗预算。只计算成功会让一个损坏的项目在一个死循环中耗尽账户。
- **两个上限都有约束力。** 低于每日上限但达到了每周上限?它会停止。
- **`--now` 会跳过窗口和闲置检查,绝不跳过预算检查。**
- **达到上限时,它会停止并说明情况**,一次,作为摘要中的一个 `skipped` 行。
从低开始。每天六次运行已经有很多审查了。
## 配置
存在于 `~/.nightaudit/config.yaml`。这里是它的全貌——这是所有的配置项:
```
providers:
claude_code:
enabled: true
budget: { max_runs_per_day: 6, max_runs_per_week: 30 }
codex:
enabled: true
budget: { max_runs_per_day: 6, max_runs_per_week: 30 }
# Optional. Only needed when the CLI isn't on PATH under its own name —
# e.g. the Codex bundled inside ChatGPT.app:
binary: /Applications/ChatGPT.app/Contents/Resources/codex
projects:
- name: gradagent
path: ~/projects/gradagent
tasks: [code_review, deps_audit, docs_drift]
- name: nightaudit
path: ~/projects/nightaudit
tasks: [code_review]
# Optional. Pin this project to one provider. Without it, whichever enabled
# provider is idle and under budget takes the project.
provider: codex
# Optional. Your own commands, run before the review. Unlike a task, these
# are NOT sandboxed — see "Checks".
checks:
- name: tests
run: pytest -q
timeout_s: 120
schedule:
windows: ["09:00-18:00", "00:00-06:00"] # local time; may cross midnight
idle_minutes: 60
digest:
dir: ~/nightaudit-reports
run:
timeout_s: 600
```
编辑后运行 `nightaudit status`——它会验证文件,并在有任何问题时准确告诉你哪里出错了。
设置 `NIGHTAUDIT_HOME` 以将整个状态目录移动到其他地方。
## 任务
一个 task 就是一个提示词模板。nightaudit 附带五个:
| task | 它寻找什么 |
| --- | --- |
| `code_review` | Bug、竞态条件和正确性问题 |
| `security_audit` | 注入、授权缺失、不安全的默认值 |
| `deps_audit` | 未固定的、过时的或有风险的依赖项 |
| `docs_drift` | 不再与代码匹配的文档 |
| `dead_links` | 指向不存在的事物的链接和图片路径 |
为每个项目分配适合它的任务——一个 Terraform repo 可能需要
`security_audit` 和 `deps_audit`,而不是 `dead_links`。
**编写你自己的:** 将任何 `.md` 文件放入 `~/.nightaudit/prompts/`,它的文件名就会成为一个有效的任务名称。使用内置的名称可以覆盖该模板。
模板必须告诉模型在每个发现前面加上 `HIGH`、`MED` 或 `LOW` 的前缀,并引用 `file:line`。解析是很宽容的——未标记的发现会被保留并归档为 `LOW`,而不是被丢弃。
## 命令
| 命令 | 作用 |
| --- | --- |
| `nightaudit init` | 检测 CLI,注册项目,写入配置,提议安装 cron |
| `nightaudit run [--now]` | 一次受限的运行。`--now` 跳过窗口+闲置检查 |
| `nightaudit watch [-n N]` | 实时跟随运行,包括由 cron 启动的运行 |
| `nightaudit digest [--date]` | 渲染 `DIGEST-YYYY-MM-DD.md` |
| `nightaudit status` | 预算条、最近的运行、下一个窗口、provider 健康状况 |
每个命令都支持 `help`。
## 故障排除
**`nothing to do — outside configured windows (00:00-06:00)`**
设计如此——它不在窗口期内。可以使用 `nightaudit run --now` 强制运行,或者扩大 `schedule.windows`。
**`nothing to do — claude_code used 12m ago (needs 60m idle)`**
你一直在使用 Claude Code,所以 nightaudit 正在避开你。可以使用
`--now`,或者降低 `idle_minutes`(`0` 表示禁用检查)。
**`nothing to do — claude_code: daily budget spent (6/6 today)`**
今天的配额用完了。如果想要更多,可以调高 `max_runs_per_day`。
**``No usable AI CLI found. Install Claude Code or Codex and re-run `nightaudit init`.``**
你的 `PATH` 中没有任何 CLI。在相同的 shell 中检查
`claude --version` / `codex --version`。
**Cron 从不运行它。** Cron 使用最小化的 `PATH`,这就是为什么 `init` 会将二进制文件的绝对路径写入你的 crontab。检查
`/tmp/nightaudit-cron.log`——cron 运行的所有内容都记录在那里。在 macOS 上,cron 可能还需要“完全磁盘访问权限”才能读取你的项目。
**运行卡住了。** 运行会在 `run.timeout_s`(默认为 600 秒)时被终止,并记录为
`timeout`。不需要手动清理任何东西。
## 卸载
nightaudit 在其他任何地方都不保留状态,因此移除它只需三行:
```
crontab -e # delete the block (see below)
rm -rf ~/.nightaudit # config, ledger, queue, event logs
pipx uninstall nightshift-cli
```
如果你最初是在 0.4.0 之前安装的,你的状态可能仍然在 `~/.nightshift` 中——nightaudit 会在那里读取它,而不是强迫你移动它。`nightaudit status` 会打印它实际使用的目录,那才是你要删除的目录。
`init` 将它的 crontab 行围在两个标记之间——删除它们以及它们之间的所有内容:
```
# nightaudit(托管 — 通过 `nightaudit init` 编辑)
...
# 结束 nightaudit
```
`~/nightaudit-reports` 中的摘要属于你——删不删由你决定。
## Providers
调度器、账本、队列和摘要都与 provider 无关。启用你拥有的任何 CLI;每个都有独立的预算。
| provider | 状态 | 如何强制只读 |
| --- | --- | --- |
| `claude_code` | 可用 | CLI 权限标志——一个只读类工具的白名单,加上每个可变工具的黑名单 |
| `codex` | 可用 | Codex 自身的 OS 沙箱——macOS 上的 Seatbelt,Linux 上的 Landlock + seccomp |
| `copilot` | 桩代码 | — 见下文 |
一个硬性要求,也是最后一列存在的全部原因:**只读保证必须由 CLI 自身的权限系统强制执行**,而不是通过善意地请求模型来实现。做不到这一点的适配器不会被合并,因为“AI 触碰 0 个文件”是整个产品的核心。
**Copilot:需要帮助,但被上游阻碍。** 它以有文档记录的桩代码形式提供
(`nightaudit/adapters/copilot.py`)。障碍不在于工作量——而在于 Copilot CLI 没有能够达到该标准的强制执行原语。它的文件级拒绝不能跨工具应用,所以 `shell(cat x)` 会绕过被拒绝的
`read(x)`,而且[一个未解决的问题](https://github.com/github/copilot-cli/issues/2722)
报告称 `--deny-tool="read(...)"` 会*无视模式*阻止*所有*读取。
一个工具遵守而另一个工具忽略的拒绝并不是权限系统。如果上游发生了改变,这个适配器只需一下午的工作量——`codex.py` 和
`claude_code.py` 都是参考模板。
## 开发
```
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
```
测试套件不消耗任何配额:调度器、预算、队列和摘要都是针对 `FakeAdapter` 进行测试的,而 Claude Code 适配器是用模拟的 `subprocess` 测试的。没有任何测试会通过 shell 调用真实的 AI CLI。
本页面上的图片是根据真实捕获的输出生成的——如果你更改了 CLI 打印的内容,请参阅 [docs/RECORDING.md](docs/RECORDING.md)。
## 许可证
MIT标签:AI编程助手, Python, 代码审查, 开发效率, 无后门, 网络可观测性, 逆向工具