A 10-minute read for developers, tech leads and the person at your company
who signs the AI invoice.

TL;DR for people who scroll

sentinel-ir turns one source file into a small JSON fact sheet built from a
real syntax tree: imports, exports, routes, env reads, network, filesystem,
process and crypto calls, dangerous syntax, a risk level and the line that
caused it. You send that to the model instead of the raw file.

0.5 adds Rust, compile-time risk for build.rs and procedural macros,
hardening against comments and hostile input, and is ~28% faster on a
35,000-file corpus. Same IR shape for every language, so the tools built on
top (an MCP server and a browser playground) didn't need a single new field.

Why bother: the "senior dev at standup" problem

Ask a senior developer what a file does and you won't get a recital of 1,200
lines. You'll get: "It's an Express handler, reads STRIPE_KEY, writes to
/tmp, and shells out to ImageMagick, which is the part that'll bite us."

That's the IR. An LLM reading the raw file is the intern reading all 1,200
lines out loud. The tokens cost money, the comments can contain "ignore
previous instructions", and the important line is somewhere around 940.

Measured on the library's own source through the MCP server (tokenizer
gpt-tokenizer, o200k_base):

file raw tokens IR tokens
src/ast-facts.js 8,439 120
src/sentinel-ir.js 2,084 133
src/tree-parser.js 891 121
examples/basic.js 150 120

The IR has a floor of about 120 tokens. On a tiny file it doesn't pay off,
and we say so. On anything you'd actually want reviewed, it's one or two
orders of magnitude.

What the output looks like

Three lines of JavaScript that call eval and child_process:

{"analysisStatus":"complete", "world":{"risk":"critical"}, "forbidden":["eval","child_process","process_spawn"], "facts":{"ast":true,"imports":["child_process"], "operations":["dynamicEval","processSpawn","childProcess"], "riskSignals":["child_process_import:require(child_process)@1", "dynamic_code:eval@2","process_spawn:cp.exec@2"]}} 

(Trimmed. The full IR also has mission, constraints, summary and the
call lists.) Every signal is kind:evidence@line, so a reviewer, human or
model, can jump straight to the proof. facts.ast: true means the file
parsed completely, so a missing category is a proven absence, not a shrug.

The rules are structural, not vibes: a route is a call on
app/router/server/api with a string path, exec is only process
spawning when it's bound to a spawning module, and a function called
evaluate is not eval. Regex-based scanners have been confusing those two
since before some of you were born.

New in 0.5: Rust

Rust is the language you pick when you'd rather argue with a compiler than
with production. It's also where a lot of new infrastructure, browsers and
crypto lives, so it's a good place to know what code can do.

The Rust analyzer produces the same IR and fact categories as JS/TS/Python.
Paths are resolved through the file's own use declarations, including
aliases, {...} lists, self, * globs and extern crate:

use std::process::Command as Cmd; fn deploy() { Cmd::new("sh").arg("-c").arg(script).status(); } // -> processSpawn, critical, process_spawn@2 

crate::, self::, super:: and a local mod fs { ... } are not mistaken
for the standard library. A few of the things it recognizes:

  • Processes: std::process, tokio::process, nix exec and fork, libc::system, duct, xshell.
  • Native code (critical): unsafe, extern blocks, #[no_mangle], asm!, libloading, Windows LoadLibrary.
  • Embedded interpreters: rhai, mlua, deno_core, v8, pyo3 run/eval.
  • Filesystem, network and env: std::fs, tokio::fs, reqwest, hyper, TcpStream, std::env::var, dotenvy.
  • Routes: axum .route("/p", get(h)), actix attributes, rocket, warp, poem, salvo.

Does it actually parse real Rust? The stock tree-sitter-rust 0.24.2
grammar rejects a surprising amount of real code: all of web-sys/js-sys
(wasm-bindgen pub type X;), safe fn, const impl, trait aliases, macro
2.0, f16/f128, and more. We ship a patched grammar that rebuilds byte
for byte from a script. Results:

corpus stock grammar patched
15,238 files, 424 crate versions (tokio, reqwest, axum, nix, libc, ring, web-sys…) 13,693 parse 15,238
2,874 files of the Rust standard library 2,724 parse 2,874

Is unsafe detection any good? We compared it with
syn, the parser behind cargo-geiger, on the
15,237 files syn parses:

construct syn sentinel-ir
unsafe impl 2,839 2,839
unsafe trait 160 160
extern blocks 6,266 6,266
unsafe fn 10,137 10,149 (12 inside wasm-bindgen extern blocks syn skips)
unsafe blocks 35,428 37,167 (the extras are in macro bodies syn doesn't look into)
files with any unsafe 4,857 all 4,857 flagged

Plus 49 labeled snippets, and 2,950 deliberately broken real files that never
crash the analyzer and never report a dangerous operation from a half-parsed
file.

New in 0.5: the code that runs before your code runs

Here's the part that should make a security team sit up. In Rust, two kinds
of code execute on the developer's machine during cargo build:

  • build.rs build scripts.
  • Procedural macros (#[proc_macro], #[proc_macro_derive], #[proc_macro_attribute]).

This is the supply-chain attack path: you never ran the dependency, you just
compiled it. That's like being pickpocketed by the IKEA manual.

Since 0.5.1, in such a file, reading or writing files at runtime, using the
network or touching a secret-like env variable raises the risk to at least
high:

#[proc_macro_attribute] pub fn m(a: TokenStream, i: TokenStream) -> TokenStream { std::fs::read_to_string("/etc/passwd"); // low -> high i } 

What the compiler reads on purpose (include!, include_str!,
#[path]) and Cargo's own variables (OUT_DIR, CARGO_CFG_*) do not raise
it, so normal build scripts don't light up like a Christmas tree. Building or
linking C at build time (cc::Build, cmake, pkg_config, vcpkg,
cargo:rustc-link-lib) is nativeCode, i.e. critical.

On 18,112 real files (crates.io and std) 0.5.1 changed 19 risk levels, all of
them build scripts or C builds, and 17 of those went to critical. Zero
analysisStatus changes.

New in 0.5: harder to fool, harder to stall

An analyzer that feeds an LLM is itself an attack surface. The person who
wrote the file you're scanning gets to pick its contents. So in 0.5.4 and
0.5.5:

  • Comments can't hide calls. require(/*x*/"child_process").exec(c) was low in 0.5.3. Ouch. Every extractor now skips comment nodes, in every language. A fuzz run inserting comments into 2,910 files: 0 fact differences.
  • Text from the code is capped and redacted. Every string that reaches the IR (routes, imports, env names, evidence) is one line and at most 128 characters. String literals in evidence become "…", so token == os.environ["API_TOKEN"] shows up as token == os.environ["…"]. Less attacker-controlled text reaches your prompt. It's less, not none, and the README says so.
  • Broken files keep their risk. When a Python or Rust file doesn't parse (one syntax error, absurd nesting), the text fallback now knows each language's dangerous names (os.system, subprocess, Command::new, unsafe, std::fs::write…). Breaking your own syntax used to be a way to look harmless. Now it just makes you look broken.
  • 5-second parse budget. tree-sitter's error recovery is superlinear on some inputs: 16 KB of -> took about 43 s in JS. Now a parse stops at 5 s and the result is incomplete with the reason. The slowest real file in a 35,000-file corpus takes 0.2 s, so only hostile input ever hits this. 4,000 mutated files: 0 crashes, 0 risk levels lowered.

New in 0.5: faster, for free

0.5.3 stopped building a JS object for every syntax node and walking the tree
a second time for metrics nobody read. The IR stayed byte-identical:

measurement 0.5.2 0.5.3
35,000 files (Rust, JS, TS, Python) 210 s 151 s
memory, 910 KB Rust file 381 MB 222 MB
memory, 744 KB Python file 188 MB 123 MB
IR differences — 0

The best optimization is deleting code. Here, nobody was even reading it.

Supply chain: we eat our own cooking

A security-flavored package shipped from someone's laptop at 2 a.m. is a
genre of horror movie. sentinel-ir is published only from CI on protected
tags, with npm provenance. Since 0.5.2 the release fails if the
provenance attestation is missing, and CI checks npm audit signatures on
npm 10 and 11 after publishing. Tests run on Node 20, 22 and 24.

How companies can use it

  • Cheaper code review bots. Triage a pull request by IR first, then send the raw source of only the files whose risk changed. The model reads 120 tokens instead of 8,000 for every file that just formats dates.
  • Dependency gates. Flag new or updated crates whose build.rs or proc macro suddenly touches the network. That's exactly the moment you want a human to look.
  • Agent guardrails. Give agents the IR through the MCP server so they reason about what code does without pasting it into the conversation.
  • One shape for four languages. JS/TS, Python and Rust come out in the same JSON. Your pipeline handles one format, not four parsers with four opinions.
const { analyze, compress } = require("sentinel-ir"); const ir = analyze(source, { target: "build.rs" }); if (ir.world.risk === "critical" || ir.world.risk === "high") flagForReview(ir); sendToModel(compress(ir)); 

The honest part

We'd rather you trust the numbers than the marketing, so here's what it
doesn't do:

  • One file at a time. Capabilities split across files are not combined.
  • Syntax, not data flow. readFileSync(req.query.f) is a file read, not "path traversal". Risk is about capabilities, not vulnerabilities, and medium (network, writes, crypto) is normal backend code.
  • Macros are token trees. Calls inside a macro's arguments are found. What a macro expands to is not, because that needs rustc and cargo expand, which a source-only analyzer doesn't run.
  • Not a scanner replacement. Code that builds module names at runtime can look harmless. low means "nothing found", not "safe". Use it next to your SAST and your reviewers, not instead of them.

What's next

Go is next on the list. Same IR, same categories, a fifth language. The goal
hasn't changed: one analyzer, one shared shape, reused everywhere (the npm
library, the MCP server and the playground), so a new language lands in
all three at once.

Try it without installing anything in the
playground, or:

npm install sentinel-ir 

Issues and merge requests are welcome on
GitLab. Bug reports that
start with "I tried to break it and…" are especially welcome.

Independent Engineering Projects

Built as independent engineering experiments across AI, software analysis, security, and autonomous systems.

Evaluated by reproducible evidence, not by the number of layers.

Explore the projects:

Sentinel Ecosystem: AutoDoc-Sentinel, Sentinel-IR, Sentinel-IR-MCP and related tools.
Agentix Lite: Independent security and system-observation project.
Find the projects: GitLab repositories | NPM packages

Support the work: ⭐ Star the repositories on GitLab.

Connect: Follow me here on DEV Community or explore the Playground.

Each project is distributed under its own license. Check the corresponding repository or NPM package for licensing terms.

Copyright © 2026 jackymenCZ. All rights reserved. Written for the community.