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.
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.
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.
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.
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.
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
Technology
Version
Used for
Go
1.23
CLI, assessment engine, adapters and local MCP server
SQLite
not stated
Local ledger of assessments, findings, evidence and approvals, through modernc.org/sqlite 1.34 (no cgo)
YAML
not stated
Configuration and scope manifests, through gopkg.in/yaml.v3
Claude Code plugin
not stated
Skills, read-only review agents, lifecycle hooks and an MCP server
SARIF
2.1.0
Report format for code scanning tools, next to HTML, Markdown, JSON and CSV
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.
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.
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.
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).
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.
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.