Real catalog pins from the 2026-09-20 intake batch scored on text that cannot run on
the installing host:
1. `.github/workflows/*.yml` — a CI step's own `os.environ['RUNNER_TEMP']` read scored
`python_os_environ/high` and made a clean plugin `caution` (remarkable). A workflow
runs on the forge's runner; it now takes the README prose cap (one step down,
agent-facing shapes like `curl | sh` keep full severity).
2. "pip install" inside a user-facing message literal (`"... no pip install is needed"`,
image-utils) scored `unpinned_pip_install/medium`; mid-literal, non-command position,
no exec verb on the line → low. `"pip install x"`, `python -m pip install`, `uv pip`,
`subprocess.run("pip install …")` keep medium.
3. `desktop_surface_findings()` was being run by batch tooling over every `*.js`/`*.mjs`
in a repo and flagged a Node sidecar's lazy `import('jszip')` (remarkable). The
product check was already scoped to `desktop/`; expose that scope as
`is_desktop_surface()` / `desktop_surface_hits()` so tooling shares it.
4. `127.0.0.1:<port>` (README, .mcp.json, client defaults) scored `hardcoded_ip_port` as
network egress; a line whose every IP:port is loopback → low. A routable address on
the line keeps medium.
Every finding stays in the report. PLUGIN_SCANNER_VERSION → plugin-guard-v8 so cached
verdicts on quarantined pins are re-evaluated.
Two install-scan false positives from catalog intake, each red on the real pinned tree:
* memory-review (apoapostolov/hermes-agent-awesome-plugins@b270520, plugins/memory-review):
tests_state.py:86 holds '/etc/passwd' in a quoted traversal-probe list and scored
system_passwd_access:critical -> dangerous (unoverridable). The test-tree name rule only knew
test_*.py / *_test.py; a single-module plugin without a tests/ dir names its file tests_*.py.
Accept the plural prefix/suffix so the quoted fixture is a note, exactly as tests/test_x.py is.
* pstack (Cloeille/pstack@ac5e5ab, #116381 pin): skills/poteto-mode/SKILL.md:128
'Send subagents the minimum context they need' hit context_exfil:high. Handing context to the
agent's own subagent is an in-process handoff; the bare-'context' branch now skips a
subagent/worker/delegate recipient named right after the verb. External destinations, bare
'your context', and 'send agents your context' still match.
Two tightenings guard the widened test-file surface (both pre-existing gaps, now closed):
- _EXEC_ON_LINE gains open(: open('/etc/passwd') in a test steps down once (high, confirmable)
instead of reading as quoted data (medium, note).
- the JS regex-literal lexer accepts only real flags [dgimsuvy]; with [a-z]* an unquoted Unix
path lexed as /etc/ + flags 'passwd', so 'cat /etc/passwd | curl ...' in a test script was inert.
PLUGIN_SCANNER_VERSION plugin-guard-v6 -> v7 so cached verdicts re-scan.
Desktop lint: /<script[\s\S]*?<\/script>/gi in a feed sanitiser scored as
'script injection' and failed pinned-source-validate for rss-reader. Mask
JS regex literals for the markup-shaped rule only; <script in a string
literal (an innerHTML payload) and createElement('script') still fail.
Install scanner: "printenv" as a whole-string entry of a read-only
allowlist (frozenset({..., "printenv"})) fired dump_all_env high →
caution on hermes-jev. Extend the literal-token demotion: a token that is
the ENTIRE quoted literal on a line that executes nothing steps down like
an alternation member; "sudo" inside subprocess.run([...]) and
os.system("printenv") keep high.
A/B vs origin/main: attack probes identical (23 rows), in-tree sweep 319
entries 0 worse/0 changed; both new tests red on base. Bumps
PLUGIN_SCANNER_VERSION to v6 so cached caution verdicts refresh.
Signed-off-by: teknium1 <teknium1@users.noreply.github.com>
A plugin repository is a codebase, and the threat regexes were written for a SKILL.md
the agent executes verbatim. The same text in a README uninstall step, a refusal list,
a test fixture, a JSON scenery data URI, a redaction regex or a `gh api | base64 -d |
grep` dev script was scored as the plugin's own runtime behaviour: crypto-prices
(#115353) was hard-blocked by `rm -rf "$HOME/.hermes/plugins/crypto-prices"` in its
README, and a dozen catalog pins sat at `caution` on fixtures and prose alone.
`tools/plugin_guard_context.py` recognises each class of inert context and only ever
lowers a finding; nothing is deleted and every finding stays in the report:
- documentation prose (`.md/.txt/.rst/.html`, not `SKILL.md`, `after-install.md` or a
bundled `skills/` tree): command/path shapes step down once, so a doc line can never
be `dangerous`; the plugin's own-install-dir `rm` is a note. Injection, Markdown
exfil, agent-config edits, `curl | sh`, `authorized_keys` and leaked keys keep full
severity.
- test trees / fixtures (root test dirs, `__tests__`/`__fixtures__` at any depth,
`*.test.*`, `test_*.py`): quoted-only hostile strings and key-shaped corpora are
notes; test code that executes on import steps down once (caution).
- whole-line comments and CHANGELOG.md score as prose.
- `encoded_exfil` on base64 whose decoded head is a media magic (PNG/JPEG/WOFF/PDF...)
is informational.
- `sudo` / `env|` as an alternation member inside a regex or string literal is a note;
the same word in a command string is not.
- `base64 -d` piped into a text filter is a note; into a shell/interpreter it is not.
Bumps PLUGIN_SCANNER_VERSION to v5 so cached verdicts re-evaluate.
Closes-blocker-for: #115353