TGPSKI/skeptic
Repo trust auditor in one stdlib-only Go binary.
what it is
skeptic scans a filesystem for the structural conditions that let one compromise cascade: mutable action refs, unsafe pull_request_target checkouts, over-permissioned automation identities, poisoned SKILL.md directives, shadowed MCP tools, lockfile and IDE-config injection. Attack-enabling conditions, not attack artifacts.
One binary is the CLI, the CI gate, the MCP server, and the loopback daemon. No container, no hosted control plane, no telemetry. go.mod has zero require entries.
COR-, DRIFT-)what it is not
skeptic covers the gaps between existing tools. It does not replace them, and it names them rather than claiming the whole surface.
- not a CVE scanneruse Trivy, Grype, osv-scanner for package vulnerabilities
- not a SAST toolno AST parsing, no dataflow taint tracking
- not a secret scanneruse Gitleaks or trufflehog for credential leaks
- not an EDRfiles at rest, not runtime behavior
proof
The corpus decides what ships
testdata/proof holds 34 fixture families — one per defect class the scanner claims to cover, from ci-trust-abuse and depbot-ci-attack-chain to agentic-skills and agentic-output-injection. Two families exist to catch the opposite failure: clean-repo and negative-benign are false-positive control, sitting in the same suite as the positives.
Every report carries a ruleset_hash — sha256 of the active rule set — next to the findings. Two runs that disagree either scanned different bytes or ran different rules; there is no third explanation to argue about. That is what makes --baseline and --diff-only safe to put in front of a merge.
ruleset_hash → same report--fail-on severity and --fail-on-score 0–100 riskskeptic scan -p . -f json --write-baseline baseline.json
skeptic scan -p . -f json --baseline baseline.json --diff-only # only what's new
proof
A clean scan is not a repair
The easy way to make a scanner stop complaining is to delete the file it complained about. testdata/repair is built so that never scores as a fix. Each instance is a small repository carrying exactly one trust-boundary defect, and tools/score-instance.sh passes a candidate patch only on a conjunction: the targeted findings are gone, exploit.sh confirms the attack path is closed independently of the scanner, behavior.sh confirms the repo still does what it was for, nothing new appears at or above the defect's severity, and no suppression artifact was added. The scorer never passes --waivers, so a waiver written by the patcher is inert.
The graders are themselves gated. tools/selftest.sh must pass before any model run: the unpatched repo must FAIL, every saint*.sh must PASS, and every liar-*.sh must FAIL — with the check that caught each liar named in the output. A suite where one saint fails or one liar slips through is broken and may not score anything.
examples/15-trust-repair — skeptic owns fixtures and graderstools/selftest.sh # gate: saints pass, liars fail tools/score-instance.sh <instance> <candidate> # JSON verdict, per check
quickstart
go install github.com/TGPSKI/skeptic/cmd/skeptic@latest
skeptic version # commit, go version, rule count
init writes the config and XDG data dir. scan is the default subcommand.
skeptic init
skeptic scan .
skeptic scan -p . --scan-style hybrid --mode ir # triage: show everything
Presets set the defaults; explicit flags always win. Resolution order is mode < preset < config file < flags.
skeptic scan -p . --preset ci -f sarif --fail-on high > skeptic.sarif
Incremental scans skip unchanged files via an mtime + size + sha256 cache, invalidated by ruleset hash.
skeptic scan --mode developer -p . --incremental --state-cache .skeptic-state.json
skeptic waive # sha256-pinned suppression for a reviewed finding
in ci
One step, nothing pulled
The action builds skeptic from source, scans with the ci preset, uploads SARIF to GitHub code scanning, and fails the step on policy violation. No container image, no marketplace dependency.
- uses: TGPSKI/skeptic@v0.2.1
- uses: TGPSKI/skeptic@v0.2.1 # advisory only
with:
scan-style: hybrid
threat-mode: machine-identity
fail-on: none
for agents
MCP, daemon, and an isolated corpus
skeptic mcp serves skeptic_scan_repo, skeptic_waive, and skeptic_ingest_url over stdio JSON-RPC, plus daemon bridge tools when pointed at a running skeptic serve. The daemon binds loopback with token auth by default and adds scheduled background scans behind an HTTP API.
Handing malicious files to an agent to "check" is how the agent gets poisoned. skeptic corpus keeps threat artifacts encrypted at rest with AES-256-GCM and scans them in isolation, reporting the delta against the detections that were expected — so a sample can prove a rule without ever entering an agent's context.
skeptic corpus init # AES-256-GCM corpus + key skeptic corpus fetch -s ./SKILL.md # store a malicious sample skeptic corpus scan --learn # scan isolated, record expected rules
rule packs
Threat intel that stays auditable
skeptic ingest turns advisories, STIX, Sigma, and YARA sources into rule packs; sign-rulepack signs them with Ed25519 detached signatures. A scan can be told to refuse anything unsigned, so a rule that changes what a merge gate blocks is traceable to a key.
skeptic gen-rule-keypair --private-out signing.key.pem --public-out signing.pub.pem skeptic sign-rulepack --rules-file ./rules.json --private-key signing.key.pem skeptic scan --rules-file ./rules.json --rules-pubkey signing.pub.pem --require-signed-rules
stack