PenSec

An open-source, local application security assessment engine and Claude Code plugin. It reports what was checked, how, with which evidence, and what was not checked.

Status
Active
Type
Developer tool
Published
Case study written
License
AGPL-3.0
Context
7 documented items
Decision
3 documented items
Implementation
8 documented items
Evidence
13 documented items
PenSec: one mark per documented item on each track.
Catalog
56 controls and 19 cross-domain edges
Built-in adapters
7, all marked implemented but unvalidated
Control statuses
9, of which only PASS and FAIL are decisive
CI
gofmt, go vet, go test and build on Linux, macOS and Windows
License
AGPL-3.0-only

Problem

Security scanners tend to report a clean result when a check did not run, and AI review tools can present a guess as a confirmed vulnerability. A self-assessment of your own project needs to separate what was proven from what was merely not found, and must not send traffic anywhere a person has not authorized.

Context

PenSec assesses the security of a repository you own. It runs as a standalone Go CLI and as a Claude Code plugin, with slash commands such as /pensec:scan and /pensec:review, read-only review agents, hooks and a local MCP server.

The default profile, local-passive, only reads the repository. It sends no traffic to any target and executes nothing from the repository: no install scripts, Makefiles or build steps. Findings, evidence and human decisions live in a local SQLite ledger that survives context compaction and session restarts.

The README states the limit plainly: PenSec is not a certified penetration test, attestation or guarantee that no vulnerabilities exist. It reports what was checked, how, with which evidence, and what was not checked.

Constraints

  • Passive unless a person approves a scopeAnything that sends traffic needs a scope manifest filled in by a person and approved from an interactive terminal. The approval is bound to the manifest hash and expires.
  • A check that did not run is not a passBLOCKED, UNSUPPORTED, SUSPECTED, INCONCLUSIVE, ERROR and STALE never count as secure. A crashed or missing scanner is reported as ERROR or BLOCKED.
  • A model cannot confirm a findingModel-proposed issues are stored as HYPOTHESIS, and the model cannot supply the evidence text.
  • Secrets are never stored in clearSnippets are redacted before they are stored, printed, reported or returned over MCP. .env and key files are recorded by name only.
  • No telemetry, no account, no cloud backend

My role

Creator and maintainer. The repository was created on 2026-10-27 and its first commit is authored under my GitHub account.

Architecture

cmd/pensec is the CLI. internal/ holds the engine, the adapters, the SQLite store, scope handling, redaction, evidence, reports, the MCP server and the hook handlers. The Claude Code side is plain files: skills/ for the /pensec:* commands, agents/ for seven read-only review agents, hooks/hooks.json and .mcp.json, all of which call the same binary.

An assessment starts with an inventory of files and routes, plans the applicable controls from the catalog, runs the adapters that cover them and records each run, finding and evidence file in the ledger. Cross-domain edges, such as identity to authorization, are created only when both sides share concrete assets, and their status comes from the linked controls.

From repository to report
  1. Inventorypensec initFile walk, route extraction, OpenAPI drift
  2. Planpensec planApplicable controls, gates and candidate edges
  3. Scanpensec scanBuilt-in adapters, optional scanners, nothing executed from the repository
  4. Record.pensec/private/state.dbFindings and hashed evidence in the local ledger
  5. DecideStatus.DecisiveOnly PASS and FAIL are decisive
  6. Reportpensec reportHTML, Markdown, JSON, SARIF 2.1.0, CSV

pensec init builds an inventory of files and routes. pensec plan selects applicable controls from the catalog. pensec scan runs the built-in and optional external adapters and records runs, findings and hashed evidence in the SQLite ledger. Controls get one of nine statuses, of which only PASS and FAIL are decisive. Cross-domain edges link controls that share assets. pensec report writes HTML, Markdown, JSON, SARIF and CSV.

Technology stack
TechnologyVersionUsed for
Go1.23CLI, assessment engine, adapters and local MCP server
SQLitenot statedLocal ledger of assessments, findings, evidence and approvals, through modernc.org/sqlite 1.34 (no cgo)
YAMLnot statedConfiguration and scope manifests, through gopkg.in/yaml.v3
Claude Code pluginnot statedSkills, read-only review agents, lifecycle hooks and an MCP server
SARIF2.1.0Report format for code scanning tools, next to HTML, Markdown, JSON and CSV

Implementation

Every adapter declares the controls it covers, its maturity and whether it needs the network. The engine runs each one through a wrapper, so an adapter that panics produces an ERROR run instead of stopping the scan or leaving its controls looking clean, and a missing optional tool is recorded as BLOCKED. Both cases have tests.

Optional scanners (gitleaks, semgrep, osv-scanner, trivy) are used only when they are already installed. They run with an argument array, a minimal environment without cloud or VCS tokens, a disposable home directory and a timeout. Semgrep's --config auto is refused because it sends project metadata to the registry; an explicit rule pack is required.

Evidence files are written with their SHA-256 in the ledger and verified on every scan and by pensec evidence verify, so a modified evidence file is reported. Human decisions, such as marking a false positive or accepting a risk with an expiry date, are recorded with a name and a note.

Technical decisions

Store AI-proposed issues as hypotheses with engine-read evidence

Problem
The review agents in the plugin can propose findings. If the model could also supply the quoted code, a mistaken or manipulated proposal could carry evidence that does not exist in the repository.
Decision
The MCP tool accepts a control id, a file path and a line range, not evidence text. The engine checks that the file is a regular file inside the project (no symlinks, no .git or .pensec paths), reads at most 40 lines itself, redacts them, records the file hash and stores the finding as HYPOTHESIS with a note that it needs human verification.
Tradeoff or limitation
Hypotheses only make a control SUSPECTED, never FAIL, so a real issue found by review stays undecided until a person or a deterministic test confirms it.

Bind network approval to the manifest hash and a human terminal

Problem
A coding agent running PenSec could, in principle, write a scope manifest and approve it, or widen the scope after approval.
Decision
`pensec scope approve` refuses to run when stdin is not a terminal, requires a name, shows every target and asks the person to type APPROVE followed by the first eight characters of the manifest SHA-256. The approval stores that hash and expires after 1 to 72 hours; any edit to scope.yaml invalidates it. In Claude Code a PreToolUse hook also asks for confirmation before these commands and before edits to the manifest.
Tradeoff or limitation
Network tests cannot be fully automated in CI; that is intended.

Check the address at connection time, not only the URL

Problem
A target that is in scope by name can resolve to a private, loopback or cloud metadata address, or redirect there.
Decision
The HTTP client resolves the host inside its dialer and rejects loopback, private, link-local, carrier-grade NAT and multicast addresses, and always rejects cloud metadata addresses. Redirects are never followed, and the client ignores any inherited proxy.
Tradeoff or limitation
Only the production-observe profile exists so far; other runtime profiles are reported as UNSUPPORTED.

Alternatives considered

  • Check the address at connection time, not only the URLPrivate addresses can be allowed only by an explicit manifest setting meant for a controlled local fixture.

Failures and difficulties

The project is one day old at the time of writing, and the open issues show what is missing for real use. The plugin expects a binary in bin/, so installing it from GitHub does not work until prebuilt binaries are published (#4). The README is being rewritten for users and contributors (#3), and a project website with a demo is planned (#5).

Security considerations

  • The SECURITY.md scope lists the failures that matter most for this tool: repository content making PenSec run commands or confirm a finding, traffic outside the approved scope, unredacted secrets, a crashed adapter producing PASS, and evidence that can be modified without failing verification.
  • Reports go through redaction, and a test checks that generated reports are redacted and valid.
  • In Claude Code, a PreToolUse hook blocks edits to .pensec/private/.
  • PenSec has not had an external security review. SECURITY.md says so.

Testing approach

Go tests cover the engine against a deliberately vulnerable fixture repository, remediation and reopening of findings, staleness after an edit, a panicking adapter, a missing tool, evidence tampering and hypothesis validation. Separate tests cover the scope guard, the observe client's redirect and private address handling, redaction, report output, the hook guard decision and the MCP protocol.

CI runs gofmt, go vet, go test and a build on Linux, macOS and Windows for every push to main and every pull request, with actions pinned to commit hashes and read-only token permissions.

Results with evidence

  • CI passed on Linux, macOS and Windows for the first commit. Evidence
  • 56 controls and 19 cross-domain edges in the catalog. Evidence

Limitations

  • All adapters are implemented but unvalidated: they pass the fixture suite in the repository, with no published benchmark.
  • Runtime profiles other than production-observe are not implemented and are reported as UNSUPPORTED.
  • No prebuilt binaries yet; the CLI must be built with Go 1.23 or later.
  • Not independently audited.
  • No users, stars or feedback exist yet to measure adoption.

Future work

Source code and references

Found something inaccurate? Tell me and it will be corrected with a note.

More

Theme