---
title: "A Security Scanner Gave Our Windows Agent Setup a D at 57/100"
canonical: https://dxdev.com/blog/scanner-graded-our-agent-config-d-every-high-was-noise/
datePublished: 2026-07-04
---
The static scanner gave one Windows agent configuration a **D**, 57/100. It reported 24 issues and marked 14 of them high severity.

A report like that can turn into a bad policy decision fast. We were evaluating a config scanner because our agent setup had one security gap we could not already explain with a spend audit. Prompts, hooks, permissions, and MCP configuration can all become an execution surface. The workstation had a public read-only knowledge connector and a browser broker. The risk category was real, even though we had not had an incident.

So we ran the cheapest possible version first:

```powershell
npx ecc-agentshield scan --path C:/Users/<username>/.claude
```

It was an offline static scan. We left off `--opus`, `--injection`, `--sandbox`, `--taint`, and `--deep`, so it cost nothing in metered spend. It returned 0 critical findings, 14 highs, and a grade saying the configuration was in bad shape.

The grade was wrong about that. It was right that one small piece was missing.

## The report described a different machine

The first high finding said `CLAUDE.md` was world-writable at mode `0o666`. On a multi-user POSIX server that would matter, because another local account could edit an instruction file an agent will later load.

This was a single-login Windows desktop. The operative permission system was Windows ACLs, not Unix mode bits. The scanner took a compatibility view of the file, turned it into an attack path, and graded that path as real without establishing that another user could write the file.

A scanner finding is a conditional claim. If an attacker can reach this object through this permission model, then this configuration can produce this consequence. The scanner reported the file attribute and skipped the conditions.

The other highs followed the same pattern. We traced each one back to the source configuration instead of accepting the severity label.

| Scanner signal | What we checked | What it actually meant |
|---|---|---|
| `CLAUDE.md` marked world-writable at `0o666` | The Windows ACL context and who could log into the workstation | A POSIX multi-user warning applied to a one-login Windows machine |
| A bootstrap hook and global instruction file flagged as attacker implants | The hook text and the global configuration we wrote | Deliberate bootstrap behavior: a `PATH` export, `&>/dev/null`, `cargo install`, and the `<!-- rtk-instructions -->` marker |
| Permission rules marked over-broad | The exact allow entries | A scoped `python3 -c` one-liner and `find … -name .env*`, which only lists names |
| A third-party `check-config.sh` script marked for possible leakage | The script body, its file reads, and its network behavior | It trims a configuration key name and reads its own config. It does not exfiltrate data or make a network call. |

The last row was the most useful diagnostic. The scanner had tripped on substrings in a clean third-party script from `last30days`. Stopping at the label would have meant treating a string match as evidence of data exfiltration. Reading the script took less time than deciding how to suppress the finding.

Coverage was the bigger problem. The scanner found three skills because it saw plugin `.md` shims. It missed roughly 40 real skills under `.claude/skills`. So the grade was not even a coherent score over the configuration that matters. It scored the subset the tool recognized, against a security model built around multi-user POSIX systems and proprietary skill conventions.

The project ships a `false-positive-audit.md`. That told us something about the tool's limits, and about what running it weekly would look like. We would spend our time re-litigating the same mismatched assumptions.

## The one finding that held up

The scan did find a cheap gap that was real on this machine. `settings.json` had an allow list and no deny list.

That does not become critical just because a scanner reported it. The existing allow rules still constrained what the agent could do. But a deny list is the right second boundary for a short set of operations we never want available through an agent session by default: force-push, `rm -rf`, and raw secret reads.

We kept the fix narrow. The goal was to make a bad operation harder to approve by accident, even if another permission rule changes later. Making the configuration look secure to a generic rubric was never the goal.

There was one piece of ordinary cleanup too. An old allow rule, `Bash(find <retired-drive>/Projects/<legacy-project> …)`, still pointed at a retired project drive, even though active projects had moved to a different one. Stale configuration makes a future review harder, so we deleted it while the file was open.

This is why we do not dismiss a noisy scan outright. A report full of false positives can still force a useful look if you separate the scanner's evidence from its conclusion. The evidence gave us an inventory of configuration edges to inspect. The conclusion did not carry over to this workstation.

## Why it did not become a build gate

The obvious next step was to run the scanner on every configuration change and fail the build on any high finding. We rejected that. A gate is only useful when its failures are more trustworthy than the people who have to investigate them. Fourteen highs that all collapsed under a basic threat-model review would train us to ignore the next report.

We also skipped the scanner's optional `--opus` tri-agent pass for a more elaborate verdict. What we needed to know was whether a plausible attacker could change the file on this Windows machine, and whether the flagged commands could do what the report implied. That took config inspection and reading the referenced script.

A recurring scanner may make sense later if the environment changes. A multi-user host, broader execution permissions, untrusted plugins, or more writable MCP definitions would change the calculation. On this setup, the recurring cost would be review fatigue on top of runtime.

The result is deliberately unglamorous. We kept the current configuration, added the missing deny-list boundary, removed the stale path, and installed no new permanent security ceremony.

Every high turned out to describe somebody else's attacker, operating system, and configuration conventions. The one useful result was a small control that should have been there anyway.

That is the bar we want for agent security tooling. Show the path from a real attacker to a real consequence on the machine we actually run. A letter grade can start the audit, but it cannot finish it, and a scanner should not gate builds until its failures are more trustworthy than the people investigating them.
