关于 HEIDES
HEIDES 是本地运行的确定性代码分析工具,为编码 Agent 构建代码图谱、生成问题报告,并在补丁应用前评估风险。它提供静态检查和结构化 MCP 结果,不依赖云端或模型。
主要功能
- 01构建持久代码图谱
- 02检查污点流与代码风险
- 03分析调用关系和导入
- 04评估补丁及受影响调用方
- 05以门禁方式检查并返回退出码
- 06提供命令行与 MCP 工具
适用场景
- 01在提交补丁前评估影响
- 02在 CI 中阻止严重问题
- 03查询函数调用方与代码文档
- 04检查输入是否流向 SQL 或 Shell
MCP 相关
`heides mcp` 通过 stdio 提供 11 个工具,包括 `harmony.check` 和 `harmony.report`,返回带有严重级别、文件位置和安全门禁信息的结果。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
HEIDES

代码的神经系统
HEIDES 是一个确定性测试框架,为 AI 编码 Agent 提供它们自身所不具备的能力:一份持久的代码地图、基于该地图生成的警告,以及补丁应用前给出的判定。Agent 提出建议,HEIDES 决定是否安全。
一个二进制文件。无需云服务、模型或账户。它可以在笔记本电脑、服务器、CI 中运行,也可以在运行 Termux 的手机上运行。
heides scan # map the codebase once
heides check # run every guard, exit non-zero on a blocker or critical
heides staged patch.diff # judge a patch before it lands
heides mcp # expose it to any MCP client
check 是一道门禁,而不是一份报告。发现阻断项或严重问题时,它会以非零状态码退出,因此遇到真实的 SQL 注入时,CI 会失败,无需任何人解析输出。工作区干净时则以 0 退出。--exit-zero 可恢复旧版始终返回 0 的行为,而 --exit-threshold=warning 会让门禁更严格。
每道防护都在本地运行,这正是整个设计的核心
heides 只报告它能根据已读取文件证实的问题。每条发现都标明文件和行号,同一棵代码树无论在笔记本电脑、容器,还是在关闭网络的手机上,都会得到相同的判定。没有会过期的缓存,也没有可能出故障的注册表。
过去并非如此。heides 曾有一个依赖项防护,会查询固定版本是否属于已知 CVE;而在工具内回答这个问题的各种方式,都可能出错:
- 通过网络查询时,门禁判定取决于网络状况,而非代码树。在同一个包含 5 个文件的测试样例中,启用该防护时
check耗时 3242 ms,不启用时耗时 117 ms。 - 使用缓存则更糟。离线缓存会在 24 小时后让原本干净的结果过期,因此早上发布的 CVE,在重新获取条目之前仍会显示“没有已知漏洞”。输出中没有任何内容能区分这两种情况。
如果一道防护无法区分“没有漏洞”和“尚未获知”,它报告的就是虚假的安全感,而虚假的安全感与通过无从区分。因此,这道防护被移除了,而不是改成可选项:即使默认安全的可选功能仍会拖慢默认流程,而即便缓存正确,它也还是缓存。
依赖项、密钥和暴露面由
GRIM 提供,该项目是公开发布的,并由持续更新的数据源支持,而非依赖此代码库的缓存。
heides deps 和 deps.* 工具仍然存在,并会如实说明这一点,这样,持有这些名称的 Agent 就能知道答案去了哪里,而不会以为工具坏了。
heides 仍能在本地验证的,是你即将进行的更改中真正重要的部分:
| 污点 | 用户输入是否到达 SQL 字符串、shell 或提示词 |
| 密钥 | 根据值的形态检测已提交的凭据,包括源代码和配置中的凭据 |
| 边缘情况 | 输入不符合预期时会触发 panic 的解包和索引模式 |
| 架构 | 外键循环、缺少主键、读取路径上的个人身份信息 |
| 配置 | .env、Dockerfile、Terraform 和清单文件中的凭据 |
这些都是关于你的代码库而非外部世界的事实,因此适合由同一个工具处理;CVE 问题则不然。
通过 MCP
harmony.check 和 harmony.report 会返回包含防护项、严重级别、消息、文件和行号的发现结果。harmony.report 会提供 security_gate 和 security_posture,让 Agent 无需解析文本就能根据判定进行门禁检查;覆盖情况回执则包含在 JSON 中,因为收到干净结果的 Agent 必须能够了解实际检查了多少内容,而无需再次调用。
这条经验在它所针对的防护移除后仍然适用。一个 MCP 服务会在同一个进程中处理每个客户端,因此会改变行为的设置必须限定在请求该设置的调用范围内,绝不能设置在进程级别。早期版本的 harmony.check 设置了一个进程全局变量,却从未将其恢复,导致一个客户端传入参数后,悄然改变了所有客户端后续请求的安全态势。
为什么存在
AI Agent 能力强大,却对代码一无所知。它可以写出完美的函数,却仍然会破坏三个自己从未读过的调用方,因为它没有代码的持久化地图。代码检查器和测试会在更改落地后才发现问题,而且只覆盖恰好运行到的路径。典型的失败案例是:Agent 修改了签名,未经过测试的调用点因此出错,测试套件却仍然全部通过,最终在凌晨两点导致生产环境故障。
HEIDES 在最关键的时刻——写入之前——弥补了这一缺口。它能即时回答其他工具都无法回答的问题:谁调用了这个函数?这个文件实际用了哪些导入?这个补丁是否与图谱冲突?用户输入是否流入了 SQL 字符串、Shell 或提示词?
主干

HEIDES 只遍历一次目录树,就会在一个嵌入式 SQLite 文件中构建持久化图谱:文件、包含所捕获文档注释的符号、调用边、导入边,以及两个 FTS5 表,一个索引名称和文档,另一个索引字面量和注释。此后的每个问题都只需查询索引,因此 Agent 查询图谱,而不是读取文件。
主干不是模型,也不是猜测。heides describe 一次性读取整个工作区清单,其中包括各语言的入口点、枢纽和文档覆盖情况。在工作区中无人调用某个函数,并不意味着它一定是死代码:Express、Flask、Django 和 Go 的路由注册都会被识别,因此处理程序不会再被报告为未调用的根节点。
成本约束由 CI 的规模阶段在每次推送时强制执行,而不是由本文件中固定的数字规定:索引时间与文件数呈线性关系,查询依赖索引,内存占用有界,分析过程绝不需要联网。
污点

污点分析是发现实际漏洞的部分。源包括各语言中的请求数据和环境读取。它会在块作用域内、跨函数边界以及跨模块级代码追踪流向,并设有跳数上限,超过上限后不再报告。接收端包括 SQL、SSRF、NoSQL、Shell、文件系统、提示词、eval 和 Django 的 mark_safe。
SSRF 和 NoSQL 刻意采用比 SQL 更严格的门槛。硬编码的健康检查 URL 和请求处理程序内的字面量查询对象都是安全的,因此这些规则要求被污染的值确实到达接收端所在行,而不只是处理程序中某处存在源:
app.get('/health', async (req, res) => {
const id = req.query.id; // a source is in scope here
const r = await fetch('https://api.internal/health'); // ...and this must stay silent
});
如果被污染的 fetch 指向云元数据地址,报告会将其标记为凭据窃取,而不是泛泛的 SSRF,因为这正是它的本质:
user controlled input reaches a cloud metadata fetch sink on this line. source at line 4
报告只会声称它能看到的情况。通过变量传入的被污染主机将被报告为 SSRF,因为该地址并未出现在代码行中,工具也不会猜测它指向哪里。
最重要的规则是保持克制。run、raw、literal 和 prepare 都是含义模糊的名称,因此只有在接收对象与数据库相关时,它们才会被视为 SQL:
db.run("DELETE FROM users WHERE id = " + id) // critical SQL finding
run(taskName) // silence, a task runner is not a database
文档查询也遵循相同规则:方法名称本身含义模糊,但接收对象并不模糊:
UserModel.find(JSON.parse(req.query.filter)) // critical NoSQL finding
items.find(i => i.id === id) // silence, that is an array method
这两种情况都有回归测试,因此这种捷径不会卷土重来。
语言
污点分析和索引是分开的,表格如实说明了各自的情况。 以下每一行的数据都经过已发布的二进制文件实测,并非根据语法列表推断得出。
| 语言 | 是否建立符号索引 | 是否进行污点扫描 |
|---|---|---|
| JavaScript、TypeScript、TSX、Python、PHP、Go、Java、C# | 是 | 是 |
| C、C++ | 是 | 否 |
| Ruby | 是 | 是 |
| HTML、CSS | 是 | 否 |
| Rust | 是 | 否 |
直到 0.16.0,TypeScript 都被悄悄地跳过了污点扫描:解析器将 .ts 映射为 typescript,而规则表中没有 typescript 对应的行,因此所有 TypeScript 文件都被跳过,没有任何提示。本系列之前,Ruby 完全无法进行污点分析,原因有三个:没有 rb 扩展名、没有源代码行,以及块检测器只识别花括号,不识别 def 和 end。Rust 会建立索引,但不会进行污点扫描,因此 Rust 代码中的 shell 或 SQL 接收点不会被报告。
.tsx 使用 TSX 语法,而不是普通的 TypeScript 语法。两者不能互换:JSX 元素会打开普通语法无法识别的节点类型,遍历因此在此处停止,所以同一段代码使用 .ts 扩展名时,得到的符号数量会多于组件文件。C 和 C++ 会建立符号和调用边索引,但不会进行污点扫描:将 sprintf 的结果写入固定缓冲区,再传给 system,不会产生任何结果。
门禁

heides staged 是 Agent 循环实际使用的部分。它检查的是差异,而不是文件:签名扩宽(不只是移除)、补丁破坏的调用方和导入、补丁引入的污点,以及项目自身的测试(如果有)。判定结果采用结构化格式,因此 Agent 无需解析自然语言即可据此设置门禁;每条发现都会附带文件和行号,并以非零状态码退出。
安装
一行安装程序,可下载适用于 Linux、macOS、Windows 和 Termux Android 的预构建二进制文件。
curl -fsSL https://raw.githubusercontent.com/AbduljabbarBXR/heides/main/scripts/install.sh | bash
使用 HEIDES_VERSION 固定版本。它接受不带前缀的版本号,并会去掉开头的 v,因此 0.16.0 和 v0.16.0 都可以使用。npm 安装程序读取同一个变量,因此一个版本固定设置即可覆盖两种安装方式。
# any published tag, bare or with a leading v
HEIDES_VERSION=v0.16.0 curl -fsSL https://raw.githubusercontent.com/AbduljabbarBXR/heides/main/scripts/install.sh | bash
或者通过 npm 安装,无需 Rust 工具链:
npm install -g heides
如果运行 heides 时缺少二进制文件,启动器会立即下载并告知你。这种回退机制本不该用到:npm 11.16 及更新版本默认会阻止依赖项生命周期脚本;在 npm 11.17 中,--allow-scripts 标志会直接中止安装,因此负责获取二进制文件的 postinstall 钩子根本不会运行。启动器不会让你重新安装(而这恰恰是刚刚失败的操作),而是运行与钩子相同的安装程序。平台表位于 install.js 中,因此只有一份。
或者使用 cargo 安装:
cargo install heides
也可以从发布页面下载适用于你平台的预构建二进制文件,或通过 cargo build --release 从源代码构建。
所有渠道的版本都相同;如果 npm 封装程序和二进制文件出现版本不一致,测试会使构建失败。
快速开始
heides scan # build the spine index
heides check # every guard, human readable
heides query callers send_order # who calls it
heides query search refresh # names, docs, literals and comments
heides describe # the workspace manifest in one shot
heides plan "add a health endpoint" # is this plan grounded in what exists
heides staged patch.diff # judge a patch before applying it
heides mcp # serve the eleven tools over stdio
以下是一个完整示例:在一个植入了两个 Bug 的小型 Express 服务上运行。
$ heides check
0 blocker(s), 3 critical, 0 warning(s), 1 info
[critical] user controlled input reaches a SQL sink ... at src/knexish.js:6
[critical] user controlled input reaches a SQL sink ... at src/knexish.js:7
[critical] user controlled input reaches a SQL sink ... at src/orders.js:9
[info] no dependency manifests found
$ heides query search users
2 hit(s)
SELECT * FROM users WHERE email = (literal) at src/knexish.js:5
DELETE FROM users WHERE id = (literal) at src/orders.js:9
$ heides plan "add a health endpoint"
feasible true
no identifier in this plan exists in the spine. this plan introduces new definitions.
existing functions to build on or replace: findUser, greet at src/clean.js:1
spine holds 4 file(s) and 5 symbol(s). index version 8.
命令参考
所有命令,一览无余。heides <command> --help 会打印任一命令的用法行。
| 命令 | 功能 |
|---|---|
scan | 将当前代码库映射到持久化索引中。其他命令会读取此命令构建的内容。 |
status | 一行显示:索引保存的文件、符号、调用边和导入信息。 |
describe | 工作区清单:语言、数量、入口点、枢纽、文档覆盖率,以及哪些守卫背后有真实语料。 |
query | callers、imports、definition、calls、neighbors、search。查询谁调用了某个符号、定义位于何处、它调用了什么,还可在名称、签名、文档、字面量和注释中进行自由文本搜索。 |
check | 运行所有守卫。质量关卡。如果发现项属于阻断级或严重级,则以非零状态码退出。 |
staged | 应用统一差异之前对其进行检查。 |
verify | 项目可以对自身作出的断言:项目自身的测试以及所有本地守卫,并提供一个可用于分支判断的布尔值。 |
db | tables、columns、reads、writes、orphans、missingindex、policies、cycles、schema、routes、touch。从代码中读取的数据库图。 |
deps | 已移除。它过去用于查询固定版本是否为已知 CVE。现在会拒绝执行,并指向 GRIM。 |
config | 扫描清单和配置文件,查找凭据及重要设置。 |
export | 将代码映射写入 Markdown 文件。此命令没有对应的 MCP 工具,因为它是文件导出,而服务没有可供导出的目标。 |
confirm | 在依赖某个软件包之前,向 crates.io 和 npm 查询其真实名称。这里唯一会主动使用网络的命令。 |
plan | 根据代码库评估计划,并展示所用证据。接受自由文本。 |
scaffold | 根据计划生成项目并建立索引。接受自由文本。 |
changed-since | 索引中修改时间晚于 Unix 时间戳的文件。返回结果与 spine.changed_since 工具相同。 |
watch | 有变更时重新建立索引并重新检查。 |
version | 打印版本。 |
mcp | 通过 stdio 运行 MCP 服务。 |
MCP 工具参考
该服务通过 stdio 向任何 MCP 客户端提供 21 个工具。除特别注明外,每个工具都有对应的命令行版本。
spine.scan。将当前代码库映射到持久化索引中。(scan)spine.query。查询谁调用了某个符号、谁导入了某个模块、定义位于何处、某个函数调用了什么,或对名称、签名、文档、字面量和注释进行全文搜索。(query)spine.describe。一次调用即可获取工作区清单:语言、计数、入口点、枢纽和文档覆盖率。(describe)spine.neighbors。查看符号的各个关联:定义及其捕获的文档、调用者和调用。(query neighbors)spine.changed_since。列出在 Unix 时间戳之后修改过的已索引文件。(changed-since)harmony.check。运行所有守卫,并提供带证据的检查结果。(check)harmony.report。以结构化 JSON 返回相同的判定结果,并包含严重性计数和 clean 标志。harmony.staged。在应用统一差异之前对其进行检查。(staged)harmony.verify。将项目自身的测试和所有守卫合并为一个布尔值。(verify)grounding.plan。根据代码库评估计划,并提供所用证据。(plan)grounding.scaffold。根据计划搭建新项目并立即为其建立索引。(scaffold) 三个deps.*工具已随守卫一起移除。调用其中一个会返回错误,说明该功能已移至 GRIM,而不是返回会让人误以为服务已损坏的未知工具错误。db.tables、db.columns、db.reads、db.writes、db.routes、db.schema、db.touch。从代码中读取数据库图。(db <subcommand>)config.scan。扫描清单和配置文件中的凭据与设置。(config)web.confirm。根据软件包注册表确认事实。(confirm)
harmony.report 没有单独的命令,因为 check 会输出相同的判定结果,而 export 会导出映射;再次拆分只会为同一个答案增加第二种写法。
安全模型
- 设计上具有确定性。每条检查结果都包含文件、行号、原因和规则 ID。没有黑箱。
- 默认在本地运行。除明确发起的依赖检查和 grounding 网络调用外,任何内容都不会离开本机。
- 无遥测、无分析、无需账户、无端口、无守护进程协议。单个进程通过 stdio 通信。
- 单个静态二进制文件,无运行时依赖。
- MCP 服务读取原始字节,因此二进制输入不会导致其崩溃,并且会限制消息大小。
用例
- 在终端中的 Agent 编码会话里提供护栏。
- 在 CI 中或通过 MCP 对 AI 生成的拉取请求进行合并前审查。
- 无需阅读整个代码树,即可让 Agent 熟悉陌生代码库。
- 审查 Agent 应用的安全性,尤其是提示词注入。
- 改善旧项目的依赖管理。
尚未完成的部分
直言不讳,并在 ROADMAP.md 中通过引用和解决状态列进行跟踪。关于这份清单,有两点值得明确说明。列在这里的缺口是你可以据此规划的限制;悄无声息地什么也不做则不是。0.15.2 中就曾出现过这种情况:对从未索引过的工作区进行检查时,报告称工作区干净。现在有两项测试明确锁定了这个特定缺陷:一项断言新构建的图可以读回其中列出的每个文件,另一项断言首次检查包含未索引问题文件的工作区时会报告该问题。
- 还没有数据库层。没有
.sql解析,没有架构图,也无法查询哪些代码会操作某个表。这是目前已知最大的缺口。请注意,缺少的是 数据库 层,而不是存储:核心已经是一个嵌入式 SQLite 文件,因此这是在现有存储上扩展,而不是引入新的存储。 - 没有 API 表面图,因此无法通过一次调用回答“
POST /users最终会写入什么”。这是重点项目。 - 对文件系统写入点之外的路径遍历,以及框架特定 shell API 的命令注入规则,尚未支持。
- 没有开放重定向、XXE、不安全反序列化或加密误用规则。
- 有少数生态系统尚未读取依赖项:Ruby、.NET、Swift、Gradle、Dart;也不会从锁文件解析传递依赖。
- 尚未索引的语言:SQL、shell、Kotlin、Swift、Scala、Dart、Dockerfile。C 和 C++ 已索引符号和调用边,但没有污点记录。Rust 已索引,但未进行污点扫描。
- 任何守卫都不会访问网络。 每项发现都可以通过文件来证明。唯一会访问网络的命令是在
grounding.plan中进行网络确认;这是调用方按名称请求的有意查询,而不是作为check一部分运行的守卫。 staged只验证冲突;它不会对修补后的树运行污点分析,因此目前无法通过关卡拦截引入数据流的补丁。
阅读输出
对这个仓库运行检查后,输出了 185 行,其中 152 行是同一句话。 这实际上是一项计数为 152 的发现;将它打印成 152 行,正是导致重要发现被淹没在信息墙中的原因。
0 blocker(s), 1 critical, 157 warning(s), 41 info
[warning] unwrap can panic when the value is not present. handle the case
instead. x152 (edge.cases) at src/deps.rs:893
advice, unproven and unranked:
[info] diagnostic console call left in the code. x10 (best.practice) at npm/bin/heides.js:17
三条规则。相同的发现会折叠,并保留计数以及第一个文件和行号,因此不会丢失任何信息。--all 会打印每一项发现。证据绝不会与建议混在一起:污点发现是一条可供你查看的代码路径;“函数跨越 N 行”则是风格意见,Agent 无法像处理前者那样对后者采取行动。顶部的计数始终描述所有发现,因此折叠绝不会粉饰结果。
--no-advice 会完全移除建议部分,适用于只需要证据、不需要其他内容的关卡。
Agent 要为此付出什么
heides 专为模型阅读而构建,因此一次调用返回的是预算,而不仅仅是答案。在这个仓库上的测量结果如下:
| 调用 | 字节数 | 令牌数 |
|---|---|---|
| 将每个 Rust 文件读取一遍 | 1,105,462 | 276,365 |
describe | 6,497 | 1,624 |
describe --brief | 5,116 | 1,279 |
带建议的 harmony.check(MCP) | 4,827 | 1,206 |
默认的 harmony.check(MCP) | 718 | 179 |
概览所需内容比阅读源代码少 169 倍。通过 MCP,harmony.check
只返回证据,而 advice: true 会请求返回风格意见,因为
Agent 无法据此采取行动,关卡也无法据此判定失败。CLI 会保留完整记录:
人在终端前会有意阅读全部内容。
两个标志可防止结果成为对话的终点:
describe的--brief会省略规则测量、文档覆盖率和未记录符号列表:这是一份供人判断关卡的记录,而不是 Agent 会据此采取行动的数据。harmony.check的max_bytes会将结果裁剪到你设定的预算。裁剪会落在行边界,并报告省略了哪些内容,因此不会将受限结果误认为完整结果。
insight
heides 自相矛盾之处,以及一项更改会影响到什么。
heides insight contradictions # signals that cannot both be true
heides insight impact <symbol> # what editing this could reach
heides insight coverage # where no rule can fire at all
heides insight baseline save # record today's findings as known
heides insight baseline # what is new since that baseline
contradictions 会比较独立计算出的信号,因此分歧是证据,而非猜测:某个路由的处理函数找不到匹配的定义;某个处理函数也被死代码根检查判定为不可达;某个文件中被调用次数最多的函数却没有注释;某个文件既没有被任何对象访问,也没有调用任何对象。
impact 回答 Agent 在每次编辑前都会提出的问题。它会报告传递调用者数量、随更改而变动的文件、受影响的路由,以及是否有测试会触及该代码,因为“四个调用者”和“四个调用者且没有测试会触及该代码”是不同的答案。
coverage 会指出干净的判定在哪些情况下毫无意义。rust: 44 file(s) indexed, no taint rule can fire on any of them 与判定结果为干净是不同的说法,而只有其中一种值得采取行动。
baseline 会将一长串发现转化为增量。新出现的发现比一直存在的发现更重要,而且发现键不会考虑行号,因此仅仅向下移动的文件不会给基线带来噪声。
回执
每次 check 结束时,都会说明实际检查了什么,即使运行结果干净也不例外:
analysed 12 of 12 indexed file(s)
languages: javascript, typescript, python
这并非装饰。没有发现的工作区和从未经过分析的工作区,打印出的内容完全相同;无法说明自己属于哪一种情况的检查,无论结果如何都不值得信任。回执还会列出无法完全覆盖的语言,并区分语言不完整的两种情况:
languages: rust
no taint rules for: rust (indexed, taint not scanned)
languages: ruby
taint scanned, no grammar: ruby (no symbols in the graph)
第一种是已建立索引但未扫描数据流。第二种是已扫描数据流,但未向图中贡献任何符号。这是两种不同的缺口;假装它们没有区别,只会让工具失去用户。
如果某次运行无法读取所有已建立索引的文件,或无法访问依赖项注册表,就会如实说明,而不会将部分结果报告为完整结果。
贡献
打开拉取请求前,请阅读 CONTRIBUTING.md。RULES.md 是约定:其中明确规定了检查工具运行的每条规则及其确切触发条件、严重性和保证,因此行为可供审查,而非偶然发生。
开发
cargo build --release
cargo test
每次推送和拉取请求都会在 CI 中运行相同的门禁:格式检查、将警告视为错误的 clippy 检查、构建和完整测试套件。测试套件包括干净语料库门禁(若任何规则在惯用代码上触发就会失败)、向解析器输入随机字节和大量花括号的对抗性测试套件、断言性能预算的规模测试阶段,以及要求输出逐字节相同且索引一致的确定性测试。
许可证
MIT。请参阅 LICENSE。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。