ai / security
Claude Security scans my repos and
reports findings. An agent triages the findings through a CLI, and each
fix lands as an ordinary change. The repo holds the
threat model, one file for each open finding, and the plan that fixes
it. So git log shows why each finding closed.
Findings
A finding has an ID, a severity, an attack path, and a recommendation. The first scan of sockeye reported 36 open findings: 4 critical, 15 high, 15 medium, and 2 low. Changes fixed 35 of them, and the threat model accepts the last one. The first scan of sand reported 3, and a later scan found one more.
Most findings named a real path from untrusted input to something I
own. Examples are a job row that could name /etc as the repo to copy
into a guest, and a model API key in the arguments of a process that
any local user could read.
Threat model
Each repo has a todo/security/README.md. Its threat model section is
my statement of who the adversaries are. I judge each finding against
it, and I paste the section into the scanner's threat model when it
changes.
sockeye lists its adversaries, most likely first:
- Code that a check runs. A change's code or any dependency can be hostile, whoever wrote the change.
- Text that an agent reads. Repo files, change text, log lines, and error events can steer an agent. An agent token is a principal to limit, not an operator.
- The internet, through the public routes and the error and perf intake.
- A leaked service credential, such as a deploy token or a runner token.
The operators are not adversaries. Each has root on every VM, so a control that only stops an operator protects nothing. A third operator changes the model, so I update the section before I add one.
The model also lists the risks I accept, each with its reason. One example: a check can write an entry into its slot's Go build cache that gives a later check a false pass. A review gates each merge, and a deploy builds from an empty cache, so I accept that risk. When a finding falls inside an accepted risk, I resolve it with that reason and change no code.
One file for each finding
todo/security/ holds one file for each open finding, named
<severity>-<finding id>-<title>.md. The file holds the attack path,
my assessment, and the criteria that a fix must meet. A plan in
todo/planned/ names the findings it fixes (see git / todo).
To find the plan for a finding, I grep todo/planned/ for its ID.
A file and a plan match a finding by its ID, not by its title. Claude Security can reword a title between two reads.
claudesec
claudesec is a small Go CLI that reads and triages findings through the Claude Security API. An agent runs it in a repo, with no browser:
claudesec findings --json # open findings, most severe first
claudesec show find_01... # one finding in full
claudesec resolve --reason fixed find_01...
claudesec resolve --reason accepted_risk --note "..." find_01...
claudesec reopen find_01...
It reads CLAUDE_SECURITY_TOKEN and CLAUDE_SECURITY_PROJECT from the
.env file of the repo. A change worktree has no .env file, so
claudesec also looks in the main checkout of the worktree. It does not
guess the project from a Git remote, because a remote can name a mirror
that no project scans.
findings follows every page of the list and prints one summary line
on stderr: the count, the project, and the time of the read. The API
pages in severity order. A finding whose severity changes during the
read can appear on two pages or on none. claudesec drops a repeat by ID
and warns about a lost finding.
The token acts as me. It sees every project I can see, and it can
resolve a finding. So an agent runs findings and show when it needs
them, and runs resolve or reopen only after I decide. The API
address comes only from the process environment, never from a .env
file. A file in a repo then cannot send the token to another host.
A triage pass compares the open list with todo/. The agent adds a
file and a plan for each finding that no plan cites. For each plan that
cites an ID that is no longer open, it runs claudesec show and trims
the plan.
The fix
A fix is a change with the security: prefix. Its description ends
with two trailers:
Reported-by: Claude Security at claude.ai/security
Fixes: find_01... and find_02...
Fixes: names finding IDs, not a CWE, because a CWE names a category.
The change deletes the finding file with git rm when the fix criteria
hold. Some fixes also need a step on a host, such as a deploy and one
job run. That finding file stays until a run on the host proves the
criteria, and the plan names the step.
After the change lands, I run claudesec resolve --reason fixed. Then
git log holds the reason before Claude Security shows the state.
git log --grep 'Fixes: find_' lists every fix.
The reason sets what a later scan does. A later scan that finds a
fixed finding again opens it again. A later scan leaves a
false_positive, accepted_risk, or duplicate finding closed. The
API keeps the first reason, so I run claudesec reopen before I
resolve a finding with a different reason.