{"_id":"@allenwu06/agentguard","name":"@allenwu06/agentguard","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@allenwu06/agentguard","version":"0.1.0","description":"Static AST linter that flags missing guards on AI-agent tool calls — unguarded shell/exec, filesystem, network, and dynamic-eval sinks with no input validation or allowlist. Conservative (low false positives). Ships as a CLI, an ESLint plugin, and a GitHu","type":"module","license":"MIT","private":false,"bin":{"agentguard":"bin/agentguard.js"},"exports":{".":"./src/index.js","./eslint-plugin":"./eslint-plugin/index.js"},"engines":{"node":">=20"},"scripts":{"test":"vitest run","test:watch":"vitest","lint":"node bin/agentguard.js","dogfood":"node bin/agentguard.js src bin eslint-plugin --fail-on low"},"keywords":["ai-agents","agent-safety","tool-calling","static-analysis","ast","linter","eslint-plugin","security","llm","guardrails","cli"],"peerDependencies":{"eslint":">=8.40.0"},"peerDependenciesMeta":{"eslint":{"optional":true}},"dependencies":{"@typescript-eslint/typescript-estree":"8.59.4"},"devDependencies":{"@typescript-eslint/rule-tester":"8.59.4","@typescript-eslint/parser":"8.59.4","eslint":"9.39.4","typescript":"5.9.3","vitest":"2.1.9"},"_id":"@allenwu06/agentguard@0.1.0","_nodeVersion":"25.9.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-0My46T6sv1Z1O39wWueqqMb1Bfcb4eYqecluKuLUq8XNFoCSJJQOm9PhsmS3Qyax7FyxVSUK6UyWepyhadvgjw==","shasum":"a8bc95e87a7f5b922e176e64a7e2f7657bb79ddf","tarball":"https://registry.npmjs.org/@allenwu06/agentguard/-/agentguard-0.1.0.tgz","fileCount":14,"unpackedSize":103136,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIAFRb8H9ul8yYvARRSmyeEuTIKdIKsWIZKL6jiWqdG80AiAYYFeD/0BnwtM5THYyCPtMf87KgVqKs7oE3B8qDFzxGA=="}]},"_npmUser":{"name":"allenwu06","email":"allenwu@umich.edu"},"directories":{},"maintainers":[{"name":"allenwu06","email":"allenwu@umich.edu"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/agentguard_0.1.0_1779208947452_0.6264222969713993"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-19T16:42:27.353Z","0.1.0":"2026-05-19T16:42:27.605Z","modified":"2026-05-19T16:42:27.915Z"},"maintainers":[{"name":"allenwu06","email":"allenwu@umich.edu"}],"description":"Static AST linter that flags missing guards on AI-agent tool calls — unguarded shell/exec, filesystem, network, and dynamic-eval sinks with no input validation or allowlist. Conservative (low false positives). Ships as a CLI, an ESLint plugin, and a GitHu","keywords":["ai-agents","agent-safety","tool-calling","static-analysis","ast","linter","eslint-plugin","security","llm","guardrails","cli"],"license":"MIT","readme":"# agentguard\n\n**A building inspector for the missing safety railings around your AI agent's\ntools.**\n\nWhen you give an AI agent tools, the AI decides what to pass into them — and\nthose values can end up running shell commands, touching your files, hitting\nthe network, or executing code. The safety railing is a check on the way in:\nvalidate the input, check it against an allow/deny list, or ask a human first.\nagentguard reads your code and points at the dangerous tool actions that have\n**no such railing** — while staying quiet about the ones you've **already**\nguarded, so it doesn't nag you about safe code.\n\n```bash\nnpx @allenwu06/agentguard ./src\n```\n\nIt comes three ways so it fits wherever you already work: a **standalone\ncommand-line tool** (plain / `--json` / `--sarif` output), an **ESLint\nplugin** (it slots into the linter most JS projects already run), and a thin\n**GitHub Action**. No install ceremony, no config required, no API key, no\nnetwork, no AI involved. It **never runs** the code it checks — it only reads\nit.\n\n---\n\n## Why this exists\n\nWhen you give an LLM (a large language model — the kind of AI behind chat\nassistants) a set of tools, every tool handler is a way in for an attacker:\nthe model — or anything that can steer it, like a prompt injection hidden in a\nweb page it fetched, a poisoned document, or a malicious sub-agent — chooses\nthe arguments, and those arguments flow into a shell command, the filesystem,\nthe network, or code execution. The single most effective defense is a\n**railing on that path**: validate the input, check it against an allow/deny\nlist, or require a human to confirm before the action happens.\n\nA [Show HN post that scanned 16 open-source AI-agent\nrepos](https://news.ycombinator.com/item?id=47947356) reported that\n**76% of tool calls had no guards at all.**\n\n> That **76% figure is from that outside scan — this tool did not measure or\n> reproduce it.** agentguard also does not publish any accuracy number for\n> itself (a code-reading tool like this cannot honestly claim one — see\n> [Limitations](#limitations)). It exists so you can run a deliberately\n> cautious check on your own code, in seconds, and see your unguarded tool\n> actions before they ship.\n\n## What it detects\n\nThis is the developer reference. agentguard reads your code with a **real\nJS/TS parser** — it builds an AST (an abstract syntax tree, the structured\nform of your code a compiler uses) rather than crude text matching. It then\nflags something **only when all of these are true**:\n\n1. the function is an **agent tool handler** — registered through an\n   agent framework API it recognizes (MCP `server.tool` / `setRequestHandler`,\n   LangChain `DynamicStructuredTool` / `StructuredTool` / `tool(fn,{…})`,\n   Vercel AI SDK `tool({execute})`, LlamaIndex `FunctionTool.from`,\n   `defineTool`/`createTool`/`registerTool`-style wrappers);\n2. a **dangerous operation** (a \"sink\" — a spot where input causes a real\n   side effect: a shell command, a file write, a network call, code\n   execution) is reachable inside it, or in a same-file helper it calls,\n   followed one hop;\n3. the dangerous argument actually traces back to the handler's\n   model-controlled input (a fixed/constant argument never counts, so it\n   never fires);\n4. **no railing is present on that path**.\n\n| Rule | Severity | What it flags |\n|------|----------|---------------|\n| `AG001` | critical | Unguarded `child_process` exec/spawn/execFile/fork from tool input |\n| `AG002` | high | Unguarded sensitive filesystem **write** (write/unlink/rm/rename/chmod, incl. `fs-extra`) from tool input |\n| `AG003` | high | Unguarded outbound **network** (fetch/axios/http(s)/got/undici) to a tool-input URL |\n| `AG004` | critical | Unguarded **dynamic code execution** (`eval` / `new Function` / `vm`) of tool input |\n| `AG005` | high | Unguarded sensitive filesystem **read** (path traversal / secret exfiltration) of a tool-input path |\n\nEach finding reports: rule id, severity, `file:line:column`, the exact\n**sink**, **why** it fired, and a concrete **remediation**.\n\n### What counts as a railing (a \"guard\") — and makes agentguard stay silent\n\nA \"guard\" is the railing referred to above. agentguard recognizes three\nfamilies of them. If one is on the path, the finding does **not** fire:\n\n- **Input validation / schema (G1)** — the tool is registered *with* a\n  schema (Zod / JSON-schema / `parameters` / `inputSchema`), or the handler\n  runs `.parse` / `.safeParse` / `.validate` / `.assert` / `.cast` (Zod /\n  Joi / Yup / ajv / superstruct / io-ts) on the input. A value reassigned\n  from a validation result is treated as untainted.\n- **Allowlist / denylist (G2)** — a membership/predicate decision on the\n  tainted value (`Set.has`, `Array.includes`, a regex `.test`,\n  `startsWith`/`endsWith`, …) that can stop the sink (an `if` + `throw` /\n  `return` / `reject`, a guard clause, a ternary) **before** the sink.\n- **Human confirmation (G3)** — an awaited/used call whose name matches\n  `confirm` / `approve` / `requireApproval` / `askUser` / `humanInTheLoop` /\n  `checkpoint` / … **before** the sink.\n\nA check that appears **after** the dangerous call does not count — order and\nreachability are respected.\n\n## The conservative philosophy (the whole point)\n\n> **A linter that cries wolf gets uninstalled.** Low false positives is not\n> a nice-to-have here — it _is_ the product.\n\nagentguard deliberately accepts **false negatives** to keep **false\npositives near zero**:\n\n- It is **silent on guarded code**, on **non-agent code**, and on **test /\n  spec / fixture files** by default.\n- It only fires when the model-controlled value plausibly **reaches** the\n  sink (literal/constant arguments never fire).\n- It does **not** guess unknown SDK shapes. If your tool is registered\n  through an API it doesn't recognize, it stays silent rather than guess.\n- It is **intra-module** and conservative about data flow. A guard in\n  another file it can't see will produce a false negative — that is the\n  intended trade.\n\nIf agentguard ever flags code you believe is safe, that's the bug that\nmatters most to us — please [report it](#feedback). We tune toward silence,\nnot noise.\n\n## Install / run\n\nZero-install via `npx`. A local path is scanned fully offline.\n\n```bash\n# scan your agent's source\nnpx @allenwu06/agentguard ./src\n\n# multiple paths\nnpx @allenwu06/agentguard ./src ./packages/agent\n\n# machine-readable for tooling / CI\nnpx @allenwu06/agentguard ./src --json\n\n# SARIF v2.1.0 (a standard scan-results format GitHub understands) —\n# upload it so findings show in GitHub's code scanning tab\nnpx @allenwu06/agentguard ./src --sarif > agentguard.sarif\n\n# stricter gate: any high or critical fails the command\nnpx @allenwu06/agentguard ./src --fail-on high     # (high is the default)\n\n# include test files in the scan (off by default)\nnpx @allenwu06/agentguard ./src --include-tests\n```\n\n### Exit codes (so you can wire it into CI)\n\nCI (\"continuous integration\" — the automated checks that run on every code\npush) reads these exit codes:\n\n| code | meaning |\n|------|---------|\n| `0`  | scan completed, gate not tripped. **Also** internal error — see below. |\n| `1`  | scan completed and a finding met/exceeded `--fail-on` (default `high`). |\n| `2`  | usage error (bad arguments). |\n\n**Fails open on purpose:** if agentguard itself breaks (a bug, a folder it\ncan't read, a file it can't parse), it prints a loud error and **exits 0**. A\nlinter that is itself broken must never block every build. A single\nunparseable file degrades the scan; it never aborts it. If you want it to be a\n*hard* stop, make it a **required** check with `--fail-on` set, so a missing\nor zero result is visible rather than silently passing.\n\n## Use it as an ESLint plugin\n\nThe plugin reuses the **exact same detection core** as the CLI — ESLint's\n`Program` node *is* an ESTree Program, so there is no re-parse and no\nbehavioral drift between the CLI and the plugin.\n\nESLint **flat config** (`eslint.config.js`, ESLint 9+):\n\n```js\nimport agentguard from \"@allenwu06/agentguard/eslint-plugin\";\n\nexport default [\n  // turn the rule on as an error everywhere\n  agentguard.configs.recommended,\n\n  // …or wire it by hand for more control:\n  {\n    plugins: { agentguard },\n    rules: {\n      \"agentguard/no-unguarded-tool-call\": [\"error\", { includeTests: false }],\n    },\n  },\n];\n```\n\nTo lint TypeScript, use `@typescript-eslint/parser` for your `.ts`/`.tsx`\nfiles as usual; plain espree covers `.js`/`.jsx`. The single rule\n`agentguard/no-unguarded-tool-call` reports one message per finding, keyed\nby the rule id (`AG001`…`AG005`) so you can `eslint-disable` a specific\nclass without silencing the rest. There is **no autofix** — choosing the\nright guard (which allowlist? validate or confirm?) is a human decision, and\na wrong autofix here is worse than the finding.\n\n## Use it as a GitHub Action\n\n```yaml\n# .github/workflows/agentguard.yml\nname: agentguard\non: [push, pull_request]\njobs:\n  agentguard:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: portfolio-foundry/agent-guard-lint@v0   # or a pinned SHA\n        with:\n          path: \"src\"\n          fail-on: \"high\"\n          sarif: \"true\"\n      - uses: github/codeql-action/upload-sarif@v3\n        if: always()\n        with:\n          sarif_file: agentguard.sarif\n```\n\nThe Action emits inline PR annotations, sets outputs\n(`total`/`critical`/`high`/`medium`/`low`/`gate`/`sarif-file`), and **fails\nopen** on internal error. No tokens or secrets required.\n\n## Configuration (optional)\n\nagentguard works with **zero config**. To tune it, add\n`.agentguardrc.json` at the repo root (or pass `--config <file>`):\n\n```json\n{\n  \"severities\": { \"AG003\": \"medium\", \"AG005\": \"off\" },\n  \"ignore\": [\"legacy/**\", \"scripts/*.js\"],\n  \"allow\": [\"AG-1a2b3c4d\"]\n}\n```\n\n- `severities` — remap a rule's severity, or set it to `\"off\"`.\n- `ignore` — path globs to skip (anchored; `*` = non-`/`, `**` = anything).\n  Note: the moat is _low false positives_, not silencing — prefer fixing or\n  `allow`-listing a reviewed finding over broad `ignore`.\n- `allow` — stable finding ids (the `AG-xxxxxxxx` in `--json`) that you have\n  reviewed and accepted; they stop gating without hiding *new* findings.\n\n## Limitations (read this)\n\nagentguard is **conservative static analysis**, not a prover. It is honest\nabout what it cannot do:\n\n- **Static analysis cannot prove reachability.** Whether a sink is *actually*\n  reachable from untrusted input at runtime is undecidable in general.\n  agentguard uses AST heuristics and a deliberately shallow, intra-module\n  taint model; it is intentionally tuned to **miss** rather than to **cry\n  wolf**.\n- **One-hop, same-module helper following only.** A sink behind a deep call\n  chain, or in another module, or behind dynamic dispatch, is a false\n  negative by design.\n- **Known-SDK recognition only.** If your tools are registered through an\n  API agentguard doesn't recognize, it stays silent (no guessing). Open an\n  issue with the shape and we'll consider adding it conservatively.\n- **Guards it can't see are assumed absent → it errs toward _not_ firing**,\n  but a guard implemented in a way it doesn't model (e.g. a custom validator\n  with an unrecognized name) can still yield a false negative.\n- **Language scope:** JavaScript and TypeScript (`.js .jsx .mjs .cjs .ts\n  .tsx .mts .cts`). Minified/bundled/`.d.ts` files are skipped (a bundle is\n  not the audited source).\n- **No autofix, no severity inflation, no invented metrics.** It reports\n  what it can defend.\n\nA clean agentguard run means \"no *unguarded* tool-call sink that this\nconservative analysis can see\" — **not** \"this agent is safe.\" Use it as one\nfast layer, not the only one.\n\n## What it does NOT do\n\nNo network. No telemetry. No API key. No LLM/AI. It never executes the code\nit scans, never resolves your `tsconfig`/project, and the analysis core is a\npure function (`source → findings`) — which is why the whole thing is unit\ntested offline.\n\n## Development\n\n```bash\nnpm ci\nnpm test           # full suite, offline, no key\nnpm run dogfood    # agentguard scans its own source → must be 0 findings\n```\n\nThe test suite proves the three pillars on committed fixtures:\n**clean = 0**, **vulnerable = every rule fires at the right location &\nseverity**, **borderline-legit = 0 false positives** (the moat). It also\nruns the ESLint plugin through the official `RuleTester` and validates the\nSARIF v2.1.0 shape GitHub code scanning ingests.\n\n## Feedback\n\nFalse positives and false negatives are the most useful thing you can send.\nSee **[FEEDBACK.md](FEEDBACK.md)**. The zero-friction path: open an issue and\nadd the **`agentguard-feedback`** label (there's an issue template). What you\nwrite is captured and read **exactly as written** — not summarized.\n\n## License\n\n[MIT](LICENSE).\n","readmeFilename":"README.md","_rev":"1-c172fb52ca61c207a7c5b120698ecc8d"}