Most security checklists are written in a way that guarantees they will be ignored, and the first one I wrote was one of those. It listed things that should be true about an application. Sessions should be invalidated when a password changes. Uploads should be validated by their content rather than their filename. Every line was correct, and I never tested a single one, because a sentence that says what should be true never says what you would do to find out.
That is the whole failure mode. You read the item, you agree with it, and agreeing feels close enough to checking that you tick it and move on. Nothing in the format ever forces the moment where you find out you were wrong.
So I rewrote the thing from the other end. Every control now comes with a step you run, and a control is not done until that step has been run and given back the result it promised.
Here is the declarative version of a session control, the kind you find in most checklists:
Verify that session tokens are invalidated when the password is changed.
And here is what it became, lightly trimmed from control 9:
Kill every session on password change or reset, not just the current one. The whole point of a reset is evicting an attacker who already has a live session, so leaving their other sessions valid defeats it. Store a sessionsValidAfter timestamp per user and reject anything issued earlier.
Verify: log in on two browsers, change the password in one, refresh the other. Expect a 401.
The second version takes thirty seconds to act on, and it can fail. That is the only difference that matters, and it is the difference between a checklist and a test.
Doing the same to every control gave me the constraint the whole project hangs on: one verification step per control, with no exceptions. There are 82 controls across three skills and 82 verification steps, and you do not have to take my word for either number, because it is countable:
for f in skills/*/SKILL.md; do
echo "$(basename $(dirname $f)): controls=$(grep -cE '^### [0-9]+\.' $f) verify=$(grep -cE '^[[:space:]]*- \*\*Verify:\*\*' $f)"
done
The totals on their own are not enough, and this is how I found out. The first version of this loop only compared them, and they matched while control 12 had no verify step at all and control 15 had two, so both sides still read 44. So the totals are now the summary, not the proof: a per-control check, ci/scripts/check-verify-steps.sh, fails any control without exactly one step, and it runs on every pull request.
A document gets read when you remember it exists, which is usually after the code is written. By then a finding means a rewrite, and a rewrite has to argue with a deadline. The same finding before the first line is written is a decision, and a decision costs nothing.
That timing is why the checklist ships as an agent skill: an instruction file that a coding agent such as Claude Code loads on its own when it is about to write the kind of code the file covers. When mine is about to write authentication, database access, an upload handler, an API endpoint, a payment flow, an LLM call or deploy configuration, it loads the security skill first and writes the code against it. Before a launch, it runs the whole thing as an audit and writes a result for each control to a file, which is a different and more honest thing to hand someone than a green summary.
Part of the audit the skill wrote when I ran it against this site. The missing headers have since been added.
It is also built so it cannot tell me what I want to hear. Its own reporting rule forbids "all secure" as an output. It reports which controls pass, which fail, and which were never checked, because unchecked is a real state, and hiding it inside a pass is how a checklist turns back into theatre.
The controls are grouped by the phase of the work where they have to be enforced, not by severity, so the group you need is the one you are already working in.
role.
Two sibling skills sit next to it in the same repository, in the same format and under the same rule about verification: one for legal and compliance questions, and one for the decisions that have to exist before the first feature commit.
It is a baseline, not a threat model. Working through all 44 security controls does not make an application secure. It makes 44 specific and common failures less likely, which is worth having and is not the same claim.
It does not cover threat modelling for your particular product, business logic flaws, cryptographic design, physical or personnel security, or anything specific to your industry's regulations. The legal skill is not legal advice and does not pretend to be, and where the exposure is real it tells you to go and find a lawyer. The CI configuration in the repository checks the subset of controls that a machine can check and nothing more, so a green pipeline is evidence about that subset and silence about the rest.
The full version of all of that is in DISCLAIMER.md, and it is worth reading before you use any of this.
Everything is public and MIT licensed at github.com/lawalOyinlola/appsec-protocols. Take the whole thing, take one group, or take the two lines that catch the bug you actually have. The controls are mapped to OWASP ASVS 5.0 in the repository, if you want to see where the coverage sits and where it does not. If you run it against something real and a control does not survive contact, tell me, because that is the feedback it needs.
If you only take one thing, take the two-browser test. Log in twice, change your password in one window, and refresh the other. If it is still logged in, your password reset does not evict anyone, including the person it was meant for.