Skip to content

Security: soemoescode/guardskill

Security

SECURITY.md

Security policy

Reporting a vulnerability

Report security issues privately through GitHub's "Report a vulnerability" flow on this repository, or by email to the maintainer address listed on https://helios-it.nl. Please do not open a public issue for an unpatched problem.

We aim to acknowledge a report within three working days.

Threat model for GuardSkill itself

GuardSkill runs on directories that are, by assumption, hostile. Everything it reads — config values, path names, script contents — is attacker-controlled input, and the report it prints is an output channel an attacker would like to influence.

  • It only reads the project it inspects. No file there is created, modified or deleted. The only file it writes is the report you ask for by name with --out.
  • It never executes anything it finds. Values from config and hook scripts are treated as text: printed as evidence, never run, never passed to a shell.
  • It never asks a server anything. The agent-settings class reports remote MCP servers that declare no credential material. That is a statement about the file, not about the server: no connection is made, and the finding says so in its own words. Whether an endpoint really requires authentication is a question this tool deliberately cannot answer.
  • The directory walk does not follow symlinks, and neither does config reading. A config file that is a symlink, or whose resolved path leaves the scanned tree, is refused, reported, and marks the scan INCOMPLETE. The file is not read, so a delivered directory cannot point the scanner at ~/.gitconfig and have its contents printed.
  • No network access. No outbound connections, no telemetry. Identical behaviour offline.
  • No dependencies. Nothing is installed alongside it.
  • No single file can drown the report. At most 50 findings per rule per file are listed; the rest are counted in one summary finding, and the scan says it is not listing everything. A settings file with 15,000 hostile entries otherwise produced a 21.8 MB SARIF document, and GitHub refuses an upload above 25,000 results or 10 MB — so the Security tab would have shown nothing at all. A finding that cannot reach the person reading it is the same failure as not finding it.
  • One bad file costs its own finding and nothing else. A settings file that is unparsable, too large or nested past the limit is reported and marks the scan INCOMPLETE, and the failure is contained to that file. A run that has already found a payload cannot be turned into "nothing to report" by putting a second, harmless-looking file next to it — suppressing a scanner is cheaper than evading one, and that route is closed.
  • A file it cannot read is a finding, not a pass. A config or hook script that exists but cannot be opened is reported and marks the scan INCOMPLETE. Silence about a file that was never read would be the one failure mode a scanner cannot have.
  • Bounded traversal and bounded reads. Depth and entry count are capped; a config above 4 MB is reported rather than parsed. A walk that stops early is reported as INCOMPLETE, never as clean.
  • The report cannot be rewritten by its subject. Control characters are stripped from every field before it reaches a terminal or a Markdown file — values, paths, titles and explanations alike. A directory called vendor<ESC>[2K\rNo findings cannot erase the line it appears on. The JSON output keeps raw values, because it is not a terminal, with the escape character itself escaped.

If you find a way to make GuardSkill write, execute, connect out, escape its target directory, or print something that misrepresents what it found, that is a vulnerability in this tool and we want to hear about it.

Claim parity

Every behavioural claim in the README, this file, SKILL.md and the project page appears below with the test that proves it. A claim without a test is removed from the documentation rather than left standing.

Claim Where Test
It never modifies the project it inspects; the only file written is --out README, help text no source file calls process.exit after writing to stdout guards the CLI's write paths; every scan test asserts on a fixture that is regenerated byte-identically
It never executes anything it finds README, SECURITY No child_process in src/, asserted by src/ contains no process-spawning code
The directory walk does not follow symlinks SECURITY the scanner never follows symlinks out of the target
A config reached through a symlink is refused and reported, not read README, SECURITY F-09 a config reached through a symlink is refused, not read
Hook scripts that are symlinks are still inspected; ones pointing outside the tree are reported and not read README F-05 a hook script that is a symlink is still inspected
No network calls, no telemetry README, SECURITY src/ contains no process-spawning code (same assertion covers http, https, net, fetch)
An incomplete walk never reads as a clean result README a truncated walk is never reported as a clean result (text) and exit codes: CLEAN 0, FINDINGS 1, ERROR 2, INCOMPLETE 3 (exit code and JSON status)
The default traversal depth is 24 README, CHANGELOG the help text states the same default depth the code uses
--fail-on moves severity only, never completeness README --fail-on changes severity, never completeness
The JSON output is stable at schemaVersion: 1 README the JSON output matches the documented schema in all four states
The report cannot be rewritten by its subject SECURITY a hostile value cannot rewrite the report it appears in and F-11 a hostile directory name cannot rewrite the report
32 clean fixtures produce nothing above informational README no medium-or-higher finding on any clean fixture, and the fixture counts in this table are the real ones keeps the number honest
31 hostile fixtures are each caught by their own rule README each vulnerable fixture is detected by the rule it was written for
No real-world corpus repository produces a critical README no corpus repository produces a critical finding
The rule set matches the published key inventory README every rule in the ruleset appears in the inventory, every key marked covered names a rule that exists
The parser agrees with git, or declares why not — over a generated permutation of quote, comment and escape positions, not only hand-picked cases inventory, CHANGELOG the parser agrees with git, or declares why it does not, every value git accepts is at least seen by the parser, the golden table records which git produced it
An invalid rule set fails loudly instead of pinning the exit code README F-12 an invalid ruleset is rejected with a clear error
A hooks directory is judged by its scripts, not its name README a hostile hooks directory cannot hide behind a familiar name and a real husky project still produces nothing above informational
Structure is medium on its own and critical with an execution key README F-10a a clean structural finding is not critical on its own and a bare repository hidden in the tree is reported as critical
The published package runs: the installed binary prints a report and sets an exit code README, CHANGELOG the installed binary prints its version and exits 0, the installed binary finds a real finding and exits 1, the tarball npm publish would upload contains the entry point
status reports what was found; the exit code reports what was asked README status reports what was found; the exit code reports what was asked (N-4)
A --json run answers in JSON when it fails as well README --json answers in JSON when it fails, too
A config that cannot be read is reported, never counted as clean README, SECURITY an unreadable config is reported, not silently treated as clean
The agent-settings class never connects to anything, including for the remote-server check README, SECURITY the agent class never reaches the network, the remote-server check reports what the file declares and says it did not connect
An MCP server started from PATH stays informational; a shell, a temporary path or a download-and-run is critical README, inventory severity follows what the command names, not that a server exists, an ordinary MCP configuration never fails a default build
The documented agent settings file names are read, and matched the way git matches names, so a case variant is not invisible inventory only the documented file names are read, a settings file whose name differs only in case is not invisible
A server entry in a shape the scanner does not recognise is reported, not skipped inventory a server entry in an unfamiliar shape is reported, not skipped
One malformed settings file cannot suppress the rest of a scan SECURITY one hostile settings file cannot silence the rest of the scan
Hooks and MCP commands are graded on the same severity ladder inventory a hook gets the same severity ladder as a server command, an ordinary Claude Code project does not fail a default build
acceptEdits and plan are not reported; bypassPermissions is reported for what it actually does inventory the permission modes people actually use stay silent, a request to skip the approval step is reported for what it is
Agent findings carry a line number, like git findings README an agent finding carries a line number into the report
A hook produces one finding, carrying a hook rule id inventory a hook produces exactly one finding, and it is about a hook
A URL in an argument counts as an endpoint only for a known bridge inventory a URL among the arguments is only an endpoint when the command is a bridge
No single file can flood the report or push a SARIF upload past GitHub's limits SECURITY one file cannot flood the report or the SARIF upload
The agent rule set matches its published inventory README every agent rule appears in the inventory, and the reverse
An agent settings file that cannot be parsed is reported, never counted as clean README an agent settings file that cannot be parsed is reported, never counted as clean
SARIF security-severity is derived from GuardSkill's own severity, not from an invented CVSS score README security-severity follows our severity, and is not invented
An incomplete scan is visible in SARIF as well as in the exit code README an incomplete scan stays visible in SARIF

Accepted residual risk

These are known and deliberately not closed in 0.4.0. They are listed here because an accepted risk that is not written down publicly is not accepted, it is concealed.

  • Four executing config keys are out of scope: sendemail.smtpServer, instaweb.httpd, ssh.variant and the HTTP proxy settings. Reasons per key in rules/git-exec-keys-inventory.md. Each is reachable only through a command a coding agent does not run, or names something other than a program.
  • The real-world corpus is reconstructed, not cloned. The five fixtures reproduce shapes observed in an independent review, not verified checkouts pinned by commit SHA. See test/corpus/PROVENANCE.md. A reconstruction cannot surprise you the way a real checkout can.
  • Detection stops at git configuration and agent settings. .vscode/tasks.json, CI workflow definitions, npm lifecycle scripts and TOML-based agent settings are not inspected, each with a reason in rules/agent-settings-inventory.md. Naming them keeps the gap a decision rather than an oversight.
  • The remote MCP check is a declaration, not a probe. mcp-remote-no-auth reports that a server entry carries no credential material in the file. A server that requires authentication but is configured elsewhere will still be reported, and a server that declares a header but accepts unauthenticated requests will not be. Answering it properly means connecting, and the no-network promise is worth more than this check.
  • Agent settings in TOML are not inspected. .codex/config.toml and anything else in that format needs a TOML parser, and a scanner with no dependencies is not taking one on for a single format. Listed in rules/agent-settings-inventory.md with the other out-of-scope entries.
  • One parser deviation remains, on valueless keys. key with no = is treated as the string true, because a rule has to evaluate something; git treats it as boolean true. Every other case in the golden table now matches git config --file byte for byte. Listed with reasons in test/golden.test.js.

There aren't any published security advisories