@agnt-gg/nope
v0.1.4
Published
NOPE — Neutralize Operations Prior to Execution. Security guardrails for AI agents with fail-closed execution backends, dangerous-operation rules, secret redaction, prompt-injection scanning, SSRF protection, telemetry, and red-team testing.
Maintainers
Readme
NOPE — Neutralize Operations Prior to Execution
Agent security in one import. Presets to start, fluent builder to customize, one method to scan everything.
npm install @agnt-gg/nopeQuick Start
import { NOPE } from '@agnt-gg/nope';
const nope = NOPE.preset('standard');
nope.check({ command: 'DROP TABLE users;' });
// → { allowed: false, violations: [{ rule: 'db-drop-table', severity: 'critical' }] }
nope.scan(contextText); // prompt injection
nope.scan({ name: 'shell', description: 'Run commands' }); // MCP tool
const safe = nope.wrap(plug.tools); // wrap toolsPresets
| Preset | Mode | Threshold | SSRF | Scanners | Telemetry |
| ---------- | ------ | --------- | -------- | -------- | --------- |
| paranoid | strict | medium | enforced | all on | on |
| standard | strict | high | on | off | on |
| minimal | strict | high | off | off | off |
| audit | audit | low | on + log | all on | on |
Presets accept overrides:
const nope = NOPE.preset('standard', { auth: { verifyToken: myFn } });Fluent builder
const nope = new NOPE()
.withRole('admin', 'critical', 'warn')
.withRole('agent', 'medium', 'strict')
.withRateLimit('1m', 60, { admin: 200, agent: 30 })
.withLockout(5)
.withSSRF({ enforced: true })
.withScanners({ homograph: true, terminalInjection: true })
.withTelemetry({ store: 'memory', retention: '7d' })
.withTrust('@agnt-gg/*');Core API
check(action, context?)
Test an action against all rules.
nope.check({ command: 'DROP TABLE users;' });
nope.check({ tool: 'exec', params: { command: 'shutdown -h now' } });
nope.check({ code: 'eval(userInput)' });
nope.check({ command: 'shutdown -h now' }, { role: 'developer' }); // identity-awarewrap(tools)
Wrap tools — inputs checked, outputs sanitized. All layers apply automatically.
const safe = nope.wrap(plug.tools);
for (const t of safe) ai.tool(t.name, t.description, t.input, t.run);Pipeline: external scanner → built-in rules → identity context → trust level → LLM approval → onBlock → execute → sanitize output.
scan(input)
One method, dispatches by input type:
nope.scan('ignore previous instructions...'); // → prompt injection
nope.scan({ name: 'shell', description: '...' }); // → MCP tool scan
nope.scan({ code: pluginSource }); // → plugin code scan
await nope.scan({ binary: '/usr/local/bin/tirith' }); // → binary checksumsanitize(value, toolName?)
Scrub secrets from any value. 17 built-in patterns (OpenAI, Anthropic, AWS, GitHub, Stripe, JWT, PEM, etc.).
const clean = nope.sanitize(toolOutput, 'my-tool');Configuration
Start with a preset, then layer on what you need. Every feature is opt-in.
Modes & thresholds
new NOPE({ mode: 'strict' }) // block violations (default)
new NOPE({ mode: 'warn' }) // log but allow
new NOPE({ mode: 'audit' }) // silent
new NOPE({ threshold: 'critical' }) // only block criticalIdentity & auth
Role-based thresholds, token verification, rate limiting, allow/deny lists, approval memory, lockout.
const nope = NOPE.preset('standard')
.withRole('admin', 'critical', 'warn')
.withRole('agent', 'medium', 'strict')
.withRateLimit('1m', 60, { admin: 200, agent: 30 })
.withLockout(5)
.withAuth({
verifyToken: async (t) => jwt.verify(t, SECRET),
allowlist: ['user_abc'],
denylist: ['user_bad'],
});
await nope.checkWithToken(action, 'eyJhbG...'); // auto-verify + check
nope.recordApproval('user_abc', 'fs-rm-rf', 'always'); // approval memorySSRF protection
DNS resolution, redirect chain validation, cloud metadata blocking (AWS, GCP, Azure, Alibaba), IPv6 private ranges, custom blocklists, enforced mode.
nope.withSSRF({
enforced: true,
customBlocklist: ['evil.internal'],
allowlist: ['safe-api.com'],
});
await nope.resolveAndCheck('https://suspicious.com');
// → { safe: false, reason: 'DNS rebinding: resolves to 10.0.0.1' }Execution backends
NOPE never silently falls back to host execution. Callers must select a backend explicitly.
- Docker is the package's isolation boundary and defaults to no network, dropped capabilities,
no-new-privileges, CPU/memory limits, and the pinnedalpine:3.20image. - SSH transports a command to a separately administered remote host. SSH is transport, not sandboxing.
- host-process is explicitly unsafe and runs as the current OS user. It requires
acknowledgeHostAccess: trueand accepts only an executable plus an argument array—never a shell command string. - WASM shell execution is unavailable. Native commands are not WASI modules, and NOPE will not downgrade them to host execution.
const sb = nope.sandbox({
backend: 'docker', image: 'node:20-slim',
pidsLimit: 256, readonlyRoot: true, user: 'nobody',
});
await sb.exec('echo hello');
// Trusted host automation only — this is not isolation.
const host = nope.sandbox({
backend: 'host-process',
acknowledgeHostAccess: true,
executable: process.execPath,
args: ['trusted-script.mjs'],
});
await host.exec();Scanners
Built-in homograph detection, terminal injection scanning, binary verification.
nope.withScanners({ homograph: true, terminalInjection: true });Telemetry & dashboard
nope.withTelemetry({ store: 'memory', retention: '7d' });
nope.report(); // → { totalChecks, blocked, riskScore, topViolations, ... }
nope.dashboard(); // → self-contained HTML dashboardSmart LLM approval
new NOPE({
llmApprove: async (cmd, violations) => {
const r = await llm.chat('Safe? ' + cmd);
return r.includes('safe') ? 'approve' : 'escalate';
},
});LLM can only raise severity, never lower. Throws → fail-safe. Verdicts: approve, deny, escalate.
All config options
| Option | Default | Description |
| ----------------- | ---------- | ----------------------------------------------------- |
| mode | 'strict' | strict blocks, warn logs, audit silent |
| threshold | 'high' | Minimum severity to block |
| onBlock | -- | Override callback (return true to allow) |
| sanitizeOutput | true | Scrub secrets from tool output |
| outputPatterns | [] | Additional secret patterns |
| onSanitize | -- | Callback when secrets redacted |
| llmApprove | -- | Smart LLM approval (approve/deny/escalate) |
| externalScanner | -- | External scanner hook (allow/warn/block) |
| trustedSources | [] | Glob patterns for trusted tools |
| identity | -- | Role-based security profiles |
| ssrf | {} | DNS, redirects, enforced mode, blocklists |
| telemetry | -- | Event tracking and reporting |
| auth | -- | Token verification, rate limiting, allow/deny, lockout |
| scanners | {} | Homograph, terminal injection, binary verification |
Built-in Rules
60+ rules across 10 categories. All active by default.
| Category | Rules | Covers | | ------------ | ----- | --------------------------------------------------------- | | filesystem | 9 | rm -rf, format, dd, shred, chmod 777, config deletion | | database | 6 | DROP, TRUNCATE, DELETE/UPDATE without WHERE, GRANT ALL | | system | 7 | shutdown, kill, iptables flush, crontab, passwd, SELinux | | credentials | 15 | API keys (OpenAI, AWS, Stripe, etc.), JWT, PEM, SSH, .env | | injection | 5 | eval, exec, Function constructor, curl|bash, base64 | | exfiltration | 4 | upload secrets, tar pipe, reverse shell, DNS tunneling | | network | 8 | Private IPs, localhost, cloud metadata, IPv6, headers | | git | 4 | force push, hard reset, clean, delete remote branch | | packages | 2 | global install, untrusted npx | | containers | 3 | privileged, rm all, host root mount |
nope.add('no-prod-db', {
description: 'Block production database writes',
severity: 'critical',
category: 'custom',
match: (a) => /prod/i.test(String(a.params?.database || '')),
});
nope.remove('pkg-global-install');Capability scoping (v0.1.4)
Most rules detect a dangerous action. A few detect dangerous data. Telling NOPE which is which is the difference between a gate and a nuisance.
By default the matcher walks every string in every argument, so a rule that catches a destructive command also fires on any argument that merely describes one — a note, a search query, a document. Declare what your tool can actually do and NOPE will only apply rules that could possibly matter:
nope.check({
tool: 'write_file',
params: { path: 'runbook.md', content: 'Step 2: reboot the host.' },
capabilities: ['fs-write'], // what this tool can DO
sink: { path: 'runbook.md' }, // which args reach an execution sink
});
// -> clean. `content` is data; no shell exists to run it.
nope.check({
tool: 'execute_shell_command',
command: 'sudo reboot',
capabilities: ['shell'],
});
// -> sys-shutdown. This one can actually do it.Capabilities: shell, code-eval, fs-write, fs-read, sql, http,
git, container, any.
Rules opt in with appliesTo. A rule with no appliesTo (or ['any']) is
never scoped — it always runs, always against full params. That is how
the credentials category stays global: a live API key is a leak wherever it
appears, including in a field no shell will ever see.
nope.add('no-prod-writes', {
description: 'Block writes to production',
severity: 'critical',
category: 'custom',
appliesTo: ['sql', 'shell'], // irrelevant to a tool that cannot write
match: (a) => /prod/i.test(String(a.params?.database || '')),
});Omitting capabilities keeps the old behaviour. Every rule runs against
every field, so upgrading loses no coverage until you opt in per tool. An
empty array is different from omitting it: capabilities: [] declares a tool
with no execution sink at all.
Violations now also report where they matched, which is what makes a false positive obvious on sight:
{ rule: 'sys-shutdown', field: 'params.content', snippet: '…Step 2: reboot the…' }snippet is deliberately omitted for credentials rules so a secret is never
copied into your logs.
Advanced
Guard a single function
const safeExec = nope.guard(async ({ command }) => execSync(command).toString());
await safeExec({ command: 'ls' }); // works
await safeExec({ command: 'shutdown -h now' }); // throwsExternal scanner
Runs before built-in rules. Verdict is authoritative. Fails open if unavailable.
new NOPE({
externalScanner: async (action) => ({
action: (await runBinary(action)).exitCode === 0 ? 'allow' : 'block',
}),
});Red team testing
50+ built-in attack vectors with fuzzing.
const r = await nope.redTeam({ attacks: 'all', iterations: 200 });
// → { passed: 187, failed: 13, coverage: { command: { tested: 48, caught: 45 }, ... } }Consent callback
new NOPE({
onBlock: async (violations, action) => {
return await prompt('Allow? (y/n)') === 'y';
},
});Sanitization modes (v0.1.0)
sanitize() supports three modes via sanitizeMode (takes precedence over the legacy sanitizeOutput boolean):
| Mode | Behavior |
| --- | --- |
| 'enforce' (default) | Redact matches in place with [REDACTED:label] |
| 'report' | Detect + fire telemetry/onSanitize, return output unmodified — used by the audit preset for true zero-mutation observation |
| 'off' | No output scanning |
Binary safety: strings longer than maxSanitizeLength (default 64 KB) or containing long unbroken base64 runs are skipped entirely — redacting inside an encoded image/file payload would silently corrupt it.
