We build The Nervous System - an MCP server that enforces behavioral guardrails on LLM agents. It has security audit tools built in. It scans for exposed secrets, hardcoded paths, and misconfigurations.
Then we published it to npm with our passwords in the source code.
How It Happened
The Nervous System has 18 tools. One of them is security_audit - it scans project files for leaked credentials, open ports, and config mistakes. It works great.
But it scans the user's files. It never scans itself.
During development on our VPS, internal references to family data directories, hardcoded /root/ paths, and personal naming conventions crept into the codebase. The security audit caught none of it because we never pointed it at its own source.
The Fix: pre_publish_audit
We wrote a tool that scans the Nervous System's own source before every npm publish. Here's the actual implementation:
function runPrePublishAudit(sourceFile) {
const findings = [];
const file = sourceFile || __filename;
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');
// 1. Hardcoded absolute paths (non-portable)
lines.forEach((line, idx) => {
if (line.trim().startsWith('//')) return;
if (line.includes('description:')) return;
if (line.match(/['\"`]\/root\//)) {
findings.push({
type: 'hardcoded_path',
line: idx + 1,
preview: line.trim().substring(0, 100),
fix: 'Use projectPath() or configurable path'
});
}
});
// 2. Personal data that should not ship
const personalPatterns = [
{ name: 'email_address', pat: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g },
{ name: 'phone_number', pat: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g },
{ name: 'ip_address', pat: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g },
];
// 3. Internal naming that should be generic
const internalTerms = [
'family-data', 'family-home', 'family-logs',
'PAPA_FULL', 'PAPA_READ', 'ARTHUR_CHAT_ID',
];
// 4. GATE: Block publish if critical findings
const critical = findings.filter(f =>
f.type === 'personal_data' ||
(f.type === 'hardcoded_path' && !f.preview.includes('description'))
);
return {
status: critical.length > 0 ? 'BLOCKED_critical_findings' : 'ready_to_publish',
total_findings: findings.length,
critical_count: critical.length,
findings: findings,
recommendation: critical.length > 0 ?
'DO NOT PUBLISH. Fix critical findings first.' :
'Clean. Safe to publish.'
};
}
The key design decisions:
-
It scans itself by default (
sourceFile || __filename) - the auditor audits itself - It skips comments and descriptions - no false positives from documentation
- It gates on severity - warnings let you publish, critical findings block you
- It returns structured data - machine-readable so CI can consume it
The Regression Test: ns-smoke-test.js
One audit tool is not enough. We also wrote a smoke test that runs after every restart and before every publish:
// Excerpt from ns-smoke-test.js
async function testSourceCode() {
const content = fs.readFileSync(sourceFile, 'utf8');
// Check for leaked passwords
const passwordPatterns = [
/KnownPassword1/i,
/KnownPassword2/i,
];
let passwordClean = true;
for (const pat of passwordPatterns) {
if (pat.test(content)) {
failed++;
passwordClean = false;
failures.push({
test: 'password_check',
error: 'Password pattern found: ' + pat.source
});
}
}
// Pre-publish mode also checks hardcoded paths
if (PRE_PUBLISH) {
const lines = content.split('\n');
let hardcodedPaths = 0;
lines.forEach((line, idx) => {
if (line.trim().startsWith('//')) return;
if (line.includes("'/root/") || line.includes('"root/')) {
hardcodedPaths++;
}
});
}
}
The smoke test also does a fresh install test - it installs the package from npm into a temp directory, starts the server, and verifies no passwords leaked:
async function testFreshInstall() {
const tmpDir = '/tmp/ns-fresh-test-' + Date.now();
fs.mkdirSync(tmpDir, { recursive: true });
execSync('cd ' + tmpDir + ' && npm init -y && npm install mcp-nervous-system');
const content = fs.readFileSync(indexPath, 'utf8');
if (/KnownPassword1|KnownPassword2/i.test(content)) {
log('FAIL: Passwords found in fresh npm install!');
}
}
The Configurable Path Fix
The third fix was making all paths configurable. Instead of hardcoding /root/family-data/, we use a projectPath() function that reads from a config file:
// Before: hardcoded, only works on our VPS
const data = fs.readFileSync('/root/family-data/roles.json');
// After: configurable, works anywhere
const data = fs.readFileSync(projectPath('data_dir') + '/roles.json');
Users drop a nervous-system.config.json in their project root and the MCP server adapts.
The Lesson
Security tools have a blind spot: themselves. If your linter does not lint its own source, if your audit does not audit its own dependencies, if your scanner does not scan its own config - you have a gap.
The fix is simple: make every security tool's first target itself.
Our publish pipeline now looks like:
-
node ns-smoke-test.js --pre-publish- regression test -
pre_publish_audittool call - source scan - Only then:
npm publish
Three layers. The passwords are gone. The paths are configurable. And the auditor now audits itself.
The Nervous System is open source. It's an MCP server that gives LLM agents guardrails, audit tools, and behavioral enforcement. If you're running AI agents in production, check it out.
npm install mcp-nervous-system
Top comments (0)