Sentinel-IR-MCP 0.4: a safer, shorter and more polite way for AI agents to look at your code
description: "What changed in sentinel-ir-mcp 0.4.0 to 0.4.3: native code is critical, files can't be swapped under the server, folder results are about 64 % smaller, and you can tell the agent what not to read. 30 secon
description: "What changed in sentinel-ir-mcp 0.4.0 to 0.4.3: native code is critical, files can't be swapped under the server, folder results are about 64 % smaller, and you can tell the agent what not to read. 30 seconds, 2 minutes for everyone, then the developer part."
- Source (GitLab, Apache-2.0): https://gitlab.com/jakubrojicek11/sentinel-ir-mcp
- Package (npm): https://www.npmjs.com/package/sentinel-ir-mcp
- The analyzer behind it: https://www.npmjs.com/package/sentinel-ir
In 30 seconds
sentinel-ir-mcp is an MCP server. It lets AI agents in Cursor, Windsurf,
Claude Desktop, VS Code or Claude Code look at a short structural summary of
your JavaScript, TypeScript and Python files instead of the whole source. The
0.4 line (0.4.0 to 0.4.3) brought:
-
0.4.0: native code (
ctypes,.nodeaddons, FFI) iscritical, and a server that runs all day no longer degrades after about 87 MB of code. - 0.4.1: a file swapped for a symlink or a hard link while the server works is refused instead of read. There are firm limits on file size and time, and big JavaScript files take seconds instead of minutes.
- 0.4.2: folder results are about 64 % smaller, and the agent is told what "risk" actually means.
-
0.4.3: you can say which files and folders the agent must not read,
with one question in
setup. Neither the agent nor the project can change that.
npx -y [email protected] setup
Tested on Linux and Windows (Node 20, 22, 24) in CI on every change.
Part one: for everyone (about 2 minutes)
What it is
An AI coding assistant usually reads your files in full. That is expensive
(AI is billed by the amount of text), slow, and the files may contain text
that tries to give the AI orders. This server hands the AI a short report per
file instead: "writes files, calls the network on line 12, risk medium".
The AI then opens only the files that matter.
It is a bit like a hotel concierge. Guests don't walk into the storage
room; they ask at the desk and get a short answer.
What 0.4 changed, in plain words
The swap trick (0.4.1). The server first looks at the list of files,
then reads them one by one. Between those two moments, someone with write
access to the folder could replace a harmless file with a shortcut to a file
outside the folder. It is like showing the doorman your ticket and then
handing it to your friend behind you. We wrote a test that does exactly this
300 times. The old version read the outside file 9 times. The new one checks
again when it opens the file, and leaked 0 times.
The same trick works with hard links: two names for the same file, one
inside the folder and one outside. The computer does not say where the other
name is, only how many there are. So the server now refuses any file with
more than one name.
Size and time limits (0.4.1). A huge file could keep the server busy for
minutes, and the AI client gives up after 60 seconds. Now there is a 2 MiB
maximum and a 30-second budget for a whole folder. If the time runs out, you
get the part that is done plus the list of what is left.
Shorter answers (0.4.2). Asking about a whole folder used to return
every detail of every file. Now it returns one line per file (risk, what it
touches, the 3 lines that caused the risk). That is roughly a third of the
size. It is the difference between asking "who is in the building?" and
getting the guest list or everyone's passport.
The AI is also told that medium is normal for a backend (it writes files and
calls APIs, so what), and that it should start with critical.
"Please don't look in there" (0.4.3). You can now tell the server which
files and folders are off limits, for example secrets or *.min.js.
setup asks once and remembers the answer. The important part is who
decides. Only you decide, not the AI and not the project. A project file
such as .sentinelignore would let a malicious repository hide its own
dangerous file from the check, which is a bit like letting the suspect write
the search warrant.
What it does not do
It is not a virus scanner, and "low" is not a guarantee. It only controls
what this server reads. If your AI client has other tools that read files,
they are not affected.
Part two: for developers
Setup and daily use
cd my-project
npx -y [email protected] setup # asks which client and what to exclude
npx -y [email protected] setup --client cursor --exclude secrets --exclude "*.min.js"
npx -y [email protected] doctor --client cursor # after restarting the client
npx -y [email protected] guide # text for AGENTS.md / client rules
setup shows the change and writes only after you confirm (--yes skips the
prompt, --dry-run writes nothing). It backs up the old file to
<file>.bak-<timestamp> and touches only the "sentinel-ir" entry. For VS Code
and Claude Code it prints their own code --add-mcp / claude mcp add
command instead. It writes [email protected], so patch releases arrive
without running setup again.
The server has three tools, all annotated read-only:
| tool | input |
|---|---|
analyze_code |
code, optional target
|
analyze_file |
path inside the allowed folder |
analyze_directory |
optional path, max_files (≤ 500), recursive, detail ("summary" / "full") |
0.4.0: native code and the parser memory leak
The dependency moved to sentinel-ir ^0.4.2, so ctypes, cffi,
process.dlopen, *.node and FFI packages are critical with native_code
in forbidden. The bigger fix for a server is the memory leak. Parse trees
were never freed, and WebAssembly memory is not garbage-collected:
stdio, 6,000 analyze_code calls, same 17.8 KB file |
calls whose analysisStatus changed |
first changed call |
|---|---|---|
| 0.3.0 + sentinel-ir 0.3.1 | 1,083 | 4,918 |
| 0.4.0 + sentinel-ir 0.4.2 | 0 | — |
The old server returned incomplete (text fallback) silently from call 4,918
on, with no error.
0.4.1: read-time checks, hard links, limits
The read path is now open first, then check the handle:
open(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK)
fstat(handle) must be a regular file
realpath(path) must be inside an allowed folder
dev/inode of handle == dev/inode of realpath
nlink == 1 on the handle and on the path
read at most maxFileBytes from the handle
Race tests, 300 analyze_directory scans while another process swaps a
listed entry:
| swap | 0.4.0 | 0.4.1 |
|---|---|---|
| file ↔ symlink to outside | 9 leaks, 35 crashed scans | 0, 0 |
| folder ↔ symlink to outside | 8 leaks, 117 crashed scans | 0, 0 |
| file ↔ hard link to outside | 262 leaks | 0 in 3 × 300 |
The hard-link row has a lesson in it. The first fix only checked nlink on
the open handle and still leaked 155 times. The attacker swaps the path back
between open and fstat, so the handle shows one link. Checking the handle
and the path closed it.
Breaking things on purpose found three more bugs: a FIFO swapped in after
listing hung the server forever (now O_NONBLOCK), a file deleted mid-scan
crashed the whole scan (now skipped.unreadable), and on Windows our first
check refused every file. Windows has two spellings of the same path
(C:\GITLAB~1\... vs the long name), and the Windows CI job caught it.
Limits, measured on valid JavaScript with sentinel-ir 0.4.3:
| size | Linux | Windows CI (Node 20/22/24) |
|---|---|---|
| 1 MiB | 4–5 s, 0.9 GB | 13–23 s, 0.8 GB |
| 2 MiB | 10–14 s, 1.6 GB | 29–40 s, 1.5 GB |
| 4 MiB | 25–29 s, 3 GB | process aborts |
So --max-file-bytes defaults to 1 MiB and is capped at 2 MiB. The time
budget defaults to 30 s (--time-budget-ms). When it runs out, no new file
starts, timedOut: true is set, and the rest is listed in notAnalyzed.
0.4.2: short entries by default
// detail: "summary" (default)
{ path, risk, analysisStatus, language, lines, operations, riskSignals, moreRiskSignals?, parseError? }
// detail: "full" → the 0.4.1 entry with all facts
riskSignals are up to 3 kind:code@line signals that set the file's risk
level. Measured again for this article with the published 0.4.3 on the MCP
SDK's own dist/cjs (174 files, SDK 1.32.2):
detail |
chars |
|---|---|
"full" |
79,974 |
"summary" |
29,059 |
That is 64 % less. In return, the tool descriptions grew by about 1,200
characters. The client loads them once per session.
Bugs found here: a 5,000-character identifier made a 5,021-character signal
(signals are now cut to 120 characters), and that cut could split an emoji
in half and leave an invalid character in the JSON. There is a regression
test for each.
0.4.3: user-only exclusions
npx -y [email protected] setup --client cursor --exclude secrets --exclude "config/prod.js" --exclude "*.min.js"
| pattern | excludes |
|---|---|
secrets |
any file or folder named secrets, anywhere, with its contents |
config/prod.js |
only this path, relative to the project folder |
*.min.js |
by mask; * and ? stay within one name |
generated/**/out |
** is any number of folders |
The patterns end up as server arguments in the client config
("args": ["-y", "[email protected]", "--exclude=secrets", "${workspaceFolder}"]).
Running setup again keeps them (Enter), - or --no-exclude clears them.
There is no !. Absolute paths, .. and the shell characters " & | < > ^ % !
are refused, so a pattern can't break the Windows cmd /c npx line.
What the agent sees:
// analyze_file { "path": "SECRETS/../secrets/nope.js" }
"Excluded by the user's settings: SECRETS/../secrets/nope.js. Do not try to read it another way."
// analyze_directory {}
{ "analyzed": 1, "skipped": { "excluded": 2, "tooLarge": [], "symlinks": 0, "unreadable": [], "hardlinks": [] }, ... }
-
analyze_filerefuses before checking whether the file exists, so the answer doesn't reveal that. -
analyze_directorycounts excluded entries and doesn't name them, becausesecrets/aws-prod.jsalready says too much. Excluded entries don't use upmax_files. - The tool descriptions mention that exclusions exist, but not which ones.
- The check runs on the requested path and the native real path, and again
at read time.
lib/../secrets/a.js,./secrets//a.js, a symlink to an excluded file and the Windows short namePRIVAT~1are all refused. On Windows and macOS matching ignores letter case. -
doctorlists the patterns, how many entries each skips right now, and warns about a pattern that matches nothing (likely a typo).
A race test ran 300 scans while files and folders were swapped for symlinks
into an excluded folder, and nothing from the excluded folder reached the
output.
The Windows short-name test first passed for the wrong reason. Node escaped
the quotes we passed to cmd /c, so the test never got a real short name.
Logging the name it used (PRIVAT~1) proves the fixed test really runs on
Windows.
Tests
npm test → 94 tests: Linux 93 pass + 1 Windows-only skip; Windows CI 92 pass + 2 skips (FIFO, chmod) on Node 20, 22, 24
Every release was also installed from npm on Node 20, 22 and 24 and called
over stdio before we called it done.
Reproduce the size numbers
mkdir mcp-04 && cd mcp-04 && npm init -y >/dev/null && npm i [email protected]
// mcp-bench.mjs
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import path from "path";
const sdk = path.resolve("node_modules/@modelcontextprotocol/sdk/dist/cjs");
const t = new StdioClientTransport({ command: "node", args: ["node_modules/sentinel-ir-mcp/bin/sentinel-ir-mcp.js", sdk] });
const c = new Client({ name: "bench", version: "1" });
await c.connect(t);
for (const detail of ["full", "summary"]) {
const r = await c.callTool({ name: "analyze_directory", arguments: { max_files: 500, detail } });
console.log(detail.padEnd(8), "files", r.structuredContent.analyzed, "chars", r.content[0].text.length);
}
await c.close();
full files 174 chars 79974
summary files 174 chars 29059
Add "--exclude", "server" before sdk in args and skipped.excluded
shows up in the result.
What's next
An opt-in --audit-log: a JSONL line per request with path, hash, status
and risk, including refused requests and never code. Feedback is welcome on
GitLab.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.