glance-scanner
v1.5.6
Published
Security scanner for AI agents. Static analysis by default (semgrep + npm audit), no API key. Optional AI semantic analysis on your own provider key.
Maintainers
Readme
Glance Scanner
Reads code and tells you what is already wrong in it. Built for the files an AI agent runs on: skills, MCP server definitions, hooks, and the repositories an agent has been pointed at.
Part of Glance. This package is the half that reads code at rest. The half that watches a running agent, Glance guard, is in early access and is not in this package.
MIT licensed. Runs locally; the only data that leaves your machine is the dependency list npm audit checks against npm's advisory database. Turn on AI analysis and your code also goes to the provider whose key you supply, and only to them.
Try it in one line
npx glance-scanner analyze --repo https://github.com/Panniantong/Agent-ReachThat clones a public repository to a temporary directory, scans it, prints a report and deletes the clone. No key, no install, no configuration.
To scan something of your own:
npx glance-scanner analyze --path . # a directory, recursively
npx glance-scanner analyze --file app.js # one fileCode is only half of it. To scan what an agent reads — MCP server configs and skill files:
npx glance-scanner surfaces --root . # MCP configs and prompt filesWhat you get without a key
Two real static engines, no API key, no account:
| Engine | Finds | Needs |
| --- | --- | --- |
| semgrep p/default | 1074 rules: injection, traversal, XSS, deserialisation, leaked credentials | one install, see below |
| rules/ in this repo | SQL injection built by string concatenation in JavaScript, which p/default misses | nothing, it ships here |
| npm audit | dependencies with published advisories | a package.json and a lockfile |
Every line number comes from the engine that found it. Nothing here recomputes
or guesses one. A finding that has no line number is printed without one —
an npm audit advisory is about a dependency, not a place in your code, so it
gets no line rather than a made-up one.
The scan names both engines every time it runs, and says why either one did not:
Static analysis (no API key). Add --ai for semantic analysis.
semgrep (p/default + glance rules): 23 findings in 19.4s
npm audit: 144 findings in 2.1sIf neither is available it says so and returns nothing. It never falls back to invented output.
A partial scan is never printed as a clean one. semgrep applies its own
ignore list, and pointed at a directory it can decline to open a single file
while still exiting 0 with "Findings: 0". So the number of files semgrep says
it scanned is checked against the number it was given, and any shortfall is
printed as a warning naming the paths — on stderr as well as in the report, and
in --json under engines. If it scanned nothing at all, the engine is
reported as not having run:
semgrep: did not run — scanned 0 of 1 path(s) — semgrep skipped everything it
was given. This is NOT a clean result.Installing semgrep
npx glance-scanner install-tools --semgrep # or: pipx install semgrepThe first semgrep run downloads its rules to ~/.semgrep. After that it runs
with no network at all. The CLI tells you when that download is about to happen.
--metrics=off is passed on every invocation, always.
If pip refuses with externally-managed-environment, that is
PEP 668: your Python is managed by
Homebrew or your distribution and pip will not write into it. --user fails the
same way, so re-running pip is not the fix. install-tools detects this,
retries through pipx — which puts semgrep in its own virtualenv, which PEP 668
allows — and if pipx is missing or fails it names the interpreter to install
rather than repeating the command that just failed.
The verdict line, and what an "audit" tag means
Every report opens with one sentence answering the question you actually have,
before any counts. It is derived from the severity mix and the rule classes,
never written by hand, and it changes with how you invoked the scan: --repo
asks whether this code can be trusted, --path asks whether it is ready to
ship.
Findings tagged [audit] come from semgrep's audit rule class. An audit
rule reports a construct worth looking at; it has not established that anything
is wrong — express.security.audit.xss.direct-response-write fires on every
res.send() of a variable, including ones whose value never leaves your
program. A run that found only audit findings will never tell you to fix
something before shipping, because nothing has asserted a defect; it will say
they are worth a look, and where to look.
A zero-finding run says which engines ran and over how many files, rather than "clean". An engine that did not run cannot have found nothing.
Known gaps, so you are not surprised by them
Measured, not assumed:
- SQL injection built with a template literal is missed.
db.all(`SELECT * FROM orders WHERE id = ${req.params.id}`)produces no finding. The+form —"SELECT * FROM users WHERE id = '" + id + "'"— is caught, byrules/js-sql-concat.yamlin this repository, whichp/defaultmisses. That rule requires+on purpose: without the requirement it flagged a correctly parameterised query whose table name is interpolated and validated against an allowlist, because semgrep's open-source taint analysis cannot see a guard likeif (!ALLOWED.includes(t)) return. Zero false positives was worth the template-literal case.--aicovers it. npm auditis skipped when a project has apackage.jsonbut no lockfile. A scanner should not write files into the tree you pointed it at, so runnpm install --package-lock-onlyyourself if you want it covered. The one exception is--repo, where the tree is a shallow clone in/tmpthat this process made and deletes: there the lockfile is generated for you, with--ignore-scripts, and the report says it happened.- semgrep's own
.semgrepignoreexcludes test directories and untracked files when it is given a directory. glance-scanner passes explicit file paths instead, so what you asked to scan is what gets scanned, and it checks semgrep's own count of scanned files afterwards. - Neither engine reasons about intent. A parameterised query that merely looks
like concatenation, or a test fixture that looks like a leaked key, will still
be reported.
--ai --filter-fpis the pass that drops those. obfuscated_textfires on legitimate mixed-script identifiers. A non-English codebase that writes a Cyrillic or Greek word next to Latin characters is reported as a homoglyph attack. This is a false positive, not intended behaviour, and it is open as #41.
We have not measured a false-positive rate against a public benchmark, so this README does not quote one.
What you get with a key
Semantic analysis reads the code the way a reviewer would. It reasons about intent rather than shape, so it covers the gaps above — including SQL built with a template literal. It costs whatever your provider charges, which for a small repository is cents.
AI_API_KEY is your AI provider's key, not a Glance account. There is no
Glance signup and nothing to pay us for. Get a key from
Anthropic,
OpenAI or
OpenRouter, and note that an API key is billed
separately from any chat subscription you may already have.
export AI_API_KEY="your provider key"
export AI_PROVIDER="anthropic" # anthropic, openai, or openrouter
npx glance-scanner analyze --path . --aiAI_PROVIDER defaults to anthropic, so if you're using Anthropic you can
skip it. If you already have ANTHROPIC_API_KEY set, for instance from
Claude Code, the scanner picks it up automatically and you can skip
AI_API_KEY too.
Add --filter-fp and each finding gets a second pass that asks whether it is
real before it reaches your report. Parameterised queries flagged as SQL
injection, test fixtures flagged as leaked secrets, and the rest of the usual
noise get dropped with a stated reason:
npx glance-scanner analyze --path . --ai --filter-fpWe have not measured the false-positive rate against a public benchmark, so this README does not quote one.
Scanning agent surfaces
Code is not the only thing an agent reads. It reads MCP server configs as
configuration and skill files as instruction, and npm audit and semgrep read
neither.
glance-scanner surfaces --root ~/.hermes --json
glance-scanner surfaces --inventory inventory.json --jsonIt reports four things about MCP entries — plain-HTTP transport to a
non-loopback host, a literal secret held inline in a config, shell
metacharacters that would actually be interpreted, and, at medium, an unpinned
npx -y-style fetch-and-run — and four about prompt files: instruction-override
phrasing, text hidden from a human reader but not from the parser, an
instruction to send local data to a network destination, and a literal
credential.
The ten category names, as surfaces --list-categories prints them:
unencrypted_transport secret_in_config command_injection_risk
unpinned_remote_exec prompt_injection hidden_instruction
exfiltration_instruction credential_leak fenced_directive
obfuscated_textBuild your map from --list-categories rather than from this list, which is a
copy and can drift.
--root walks a directory and reads thirteen filenames — six MCP configs and
seven prompt files:
| MCP configs | Prompt files |
| --- | --- |
| .mcp.json | SKILL.md |
| mcp.json | AGENTS.md |
| mcp_servers.json | CLAUDE.md |
| claude_desktop_config.json | GEMINI.md |
| settings.json | SYSTEM.md |
| .claude.json | PROMPT.md |
| | INSTRUCTIONS.md |
.claude.json is Claude Code's user-scope MCP config and lives at
$HOME/.claude.json, a sibling of ~/.claude/ rather than inside it, so a scan
of ~/.claude alone does not reach it.
Findings carry no matched text by default. Pass --evidence to see it. The
default is set in the engine rather than left to the caller because the caller
is sometimes an LLM prompt, and quoting an injection payload into an agent's
context is delivering it.
Fenced code blocks are treated as documentation, so a security page can quote
an attack without tripping the scanner reading it. A directive found inside a
fence is not dropped: under the default balanced policy it is reported as
fenced_directive at medium, and --policy strict reports it as written.
Concealed characters are fence-immune under both, because a fence does nothing
to hide a homoglyph.
A ninth and tenth check, fenced_directive and obfuscated_text, round out ten
categories. obfuscated_text needs no phrase list: it fires on a zero-width
character between two ASCII letters, a Cyrillic or Greek letter inside an
otherwise Latin word, or a bidirectional control (Trojan Source,
CVE-2021-42574). The mixed-script rule is currently too broad — see Known gaps
below and #41.
Configuration is never read from the tree being scanned, and there is no off
level.
Full rules, schema and limitations: src/surfaces/README.md.
The extra Python scanners
These are optional and separate from the default scan. Install them once and glance-scanner will call them and merge their findings, deduplicated.
npx glance-scanner install-tools| Tool | Finds | Applies to | | --- | --- | --- | | detect-secrets | API keys, tokens, credentials | every file type | | bandit | command injection, unsafe file operations, hardcoded passwords | Python | | pylint | undefined names, unused imports, errors | Python | | pip-audit | dependencies with known vulnerabilities | requirements.txt, pyproject.toml |
Then add them to a scan:
npx glance-scanner analyze --path . --with-all-checksOr one at a time with --with-secrets, --with-bandit, --with-linting,
--with-dependencies. These need Python on your machine. Nothing else here
does.
Every flag
One table per subcommand, taken from --help on the published binary. Measured
against 1.5.5.
surfaces
| Flag | What it does |
| --- | --- |
| --inventory <path> | inventory JSON produced by a platform adapter |
| --root <dir> | discover surfaces under a directory instead |
| --json | emit the report as JSON |
| --evidence | include matched text on each finding; off by default |
| --list-categories | print the categories this engine can emit, as JSON, and exit |
| --policy <level> | balanced (default) or strict; there is no off level |
analyze
| Flag | What it does |
| --- | --- |
| --file <path> | scan one file |
| --path <dir> | scan a directory, recursively |
| --repo <url> | clone a repository to a temporary directory, scan it, delete it |
| --ai | semantic analysis, needs AI_API_KEY (or ANTHROPIC_API_KEY) |
| --semantic | alias for --ai |
| --semantic-only | deprecated alias for --ai |
| --filter-fp | second pass that drops findings it judges unreal, needs --ai |
| --with-secrets | add detect-secrets |
| --with-bandit | add bandit |
| --with-linting | add pylint |
| --with-dependencies | add pip-audit |
| --with-all-checks | add all four |
| --json | machine-readable report on stdout |
| -v, --verbose | print the code around each finding |
| --no-cache | rescan even if the content has not changed |
install-tools
| Flag | What it does |
| --- | --- |
| --semgrep | install semgrep only (powers the default no-API-key scan) |
| --secrets | install detect-secrets only |
| --bandit | install bandit only |
| --linting | install pylint only |
| --dependencies | install pip-audit only |
-V, --version and -h, --help work at the top level and on every
subcommand.
Caching applies to --ai only, where it is keyed by file content so scanning
the same code twice costs nothing. The cache is a JSON file in your home
directory and --no-cache skips it. The static engines are not cached: they
are local and fast, and a stale security result is worth less than the seconds
it saves.
Exit codes
Measured on 1.5.5, surfaces:
| run | exit |
| --- | --- |
| clean | 0 |
| info only | 0 |
| medium only | 0 |
| high or critical | 1 |
| bad --root | 2 |
A CI check on $? therefore treats info and medium as clean. If you
want those to fail a build, read the JSON rather than the exit code.
Exit 2 on a root that does not exist is deliberate (since 1.5.2): a scan that looked at nothing must never report clean. It is distinct from 1 so a pipeline can tell "found something" from "could not run".
This contract is under review; if it changes it will be a documented breaking change.
What it looks for
sql_injection, command_injection, path_traversal, hardcoded_secrets, xxe_attack, xss, csrf, insecure_deserialization, weak_crypto, missing_auth, hardcoded_config, unvalidated_redirect, information_disclosure, insecure_random, unsafe_pickle, vulnerable_dependency.
semgrep rules that do not map onto one of these keep their own rule id as the category rather than being forced into a bucket they do not fit. Coverage of each depends on the mode you ran in. See the two sections above.
Building from source
You do not need this to use the scanner. npx is enough.
git clone https://github.com/golem-labs-etc/agent-security-scanner.git
cd agent-security-scanner
npm install
npm run build
node dist/cli.js analyze --path .
npm testReporting something
Bugs and false positives both go to the issue tracker, or to [email protected]. If the scanner flagged your project and it was wrong, we want the case, because that is the part that is hard to get right.
License and scope
The scanner in this repository is MIT licensed. Use it, fork it, ship it, sell it. That is deliberate: a tool that claims to run locally and upload nothing is only worth believing if you can read it.
Glance guard and Glance fixes are not covered by this licence. They are separate products and no part of them lives in this repository. Glance guard is free for one developer on your own model key, which is a price, not a licence. Nothing here grants a right to their source.
MIT. Golem Labs.
