Dev.to Security 🔐 Cybersecurity 👁 0 📖 8 min read

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."

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:

  1. 0.4.0: native code (ctypes, .node addons, FFI) is critical, and a server that runs all day no longer degrades after about 87 MB of code.
  2. 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.
  3. 0.4.2: folder results are about 64 % smaller, and the agent is told what "risk" actually means.
  4. 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_file refuses before checking whether the file exists, so the answer doesn't reveal that.
  • analyze_directory counts excluded entries and doesn't name them, because secrets/aws-prod.js already says too much. Excluded entries don't use up max_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 name PRIVAT~1 are all refused. On Windows and macOS matching ignores letter case.
  • doctor lists 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.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.