AI编程代理或shell命令写入文件时往往不带格式化,该工具自动调用项目已有Prettier/Ruff/gofmt等工具处理写入的文件,不影响原命令输出。
编辑器早已有了"保存时格式化"功能。开发环境中的其他环节却一直没有跟上。
当生成器、Shell 命令或编码 Agent 写入文件时,字节内容通常就是生成时的原始状态。仓库里可能已经有 Prettier、Ruff 或 gofmt,但没有任何东西触发它们运行。这种失误往往要等到 pre-commit hook、CI,或者 Agent 自己换了个任务之后才会被发现。
我开发了 onwrite,在写入的那一刻就运行项目自己的工具。
我最常用的命令是:
$ onwrite run -- ./scripts/codegen.sh
onwrite: formatted src/client.ts (eslint, prettier)
onwrite: formatted api/models.py (ruff-check, ruff-format)
onwrite run 等待被包装的命令执行完毕,推算出它修改了哪些文件,然后只格式化这些文件。命令的输入、输出和退出码保持不变。
还有一个用于无法被包装的写入场景的监听模式:
$ onwrite watch
以及一个诊断命令,因为自动检测只有在能解释自己的时候才有价值:
$ onwrite doctor
onwrite 不自带格式化规则。它去找仓库里已有的选择:Prettier 配置、pyproject.toml 中的 Ruff 设置、.clang-format、Cargo.toml、Go 工具链,等等。项目本地的二进制文件会在全局之前被解析,所以锁定的版本优先。
实现上刻意保持了谨慎。格式化工具永远不会收到真实的源文件。内容通过 stdin 传入,结果通过 stdout 返回,只有在格式化链成功之后才会原子性地替换文件。
这个设计处理了我最在意的失败场景。如果监听器在半途捕获到了一个 heredoc,生成的不完整文件通常会解析失败。格式化工具报错退出,onwrite 保持文件不动。超时的格式化工具会被终止。成功退出但返回空内容的格式化工具不被允许清空文件。
监听器还避免了常见的"格式化工具写了文件,所以再次运行格式化工具"的循环,而没有使用盲等延迟。它记录了写入的确切内容。它自己的写入会被忽略,而紧接着进行的真实编辑由于内容不同,仍然会被处理。
对于 Claude Code,有一个 hook 会在格式化工具改变了 Agent 刚写入的文件时报告。这样 Agent 就有机会重新读取文件,而不是继续使用一份过时的副本。核心工具并不绑定在 Claude 上;任何能在 Shell 中运行的东西都可以放在 onwrite run -- 后面。
首个版本有 87 个测试用例,覆盖了部分写入、工具挂起、忽略文件和写入循环等场景。CI 在 Linux 和 macOS 上运行了竞态检测和针对真实格式化工具的集成测试。发布版二进制文件可用于 macOS、Linux 和 Windows。
目前还是 v0.1.0。我预计真实的仓库会暴露出我还没见过的组合。
在 macOS 或 Linux 上安装:
curl -fsSL https://raw.githubusercontent.com/hunr-ai/onwrite/main/scripts/install.sh | sh
onwrite doctor
onwrite run -- your-command-here
该项目采用 MIT 许可:github.com/hunr-ai/onwrite。
我在自己的网站上写了一篇更详细的关于安全模型以及 run、工具检测和 Agent 同步决策背后的考量:《Format-on-save, after the editor》。
如果你试用了它,告诉我它的检测哪里出错了。这种反馈最有可能改进下一个版本。