kramme-cc-workflow/skills/kramme:code:harden-security/SKILL.md
Apply security-by-default when writing code that handles user input, authentication, data storage, or external integrations. Use when building features that accept untrusted data, manage user sessions, or call third-party services. Complements the review-time auth-reviewer / data-reviewer / injection-reviewer agents with author-time guardrails.
npx skillsauth add abildtoft/kramme-cc-workflow kramme:code:harden-securityInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Apply security-by-default at author time. This is the procedural counterpart to the review-time security agents: instead of catching vulnerabilities after they're written, bake the guardrails in while the code is being authored. Retrofitting security is roughly an order of magnitude more expensive than writing it in the first place — the goal here is that common classes of vulnerability never reach the review stage at all.
Code examples in this skill use TypeScript/Node idioms (Zod, npm audit, crypto.timingSafeEqual). The underlying rules are stack-agnostic — translate to the equivalent in your ecosystem (Pydantic, go-playground/validator, Rails strong params, Go's crypto/subtle, etc.). Calls to kramme:auth-reviewer, kramme:data-reviewer, and kramme:injection-reviewer assume the Claude Code agent runtime.
innerHTML, eval, exec, or raw SQL interpolation.Trust-boundary work always wins over the negative triggers — if a refactor moves a validation point, this skill applies.
Four markers anchor this skill's output. Only SIMPLICITY CHECK is mandatory per slice; the other three appear when their triggering condition is present.
SIMPLICITY CHECK: <the simplest security measure that satisfies the threat model>
Mandatory per slice. State the smallest coherent safeguard before adding more layers. Over-engineered auth/crypto/validation stacks are themselves a security liability — complexity hides bugs. Only expand beyond the simplest version if a concrete threat forces it.
NOTICED BUT NOT TOUCHING: <what you saw>
Why skipping: <out-of-scope / unrelated / deferred>
Emit when you notice an existing insecure pattern in adjacent code (a missing auth check three lines above, a colleague's md5 helper, a secret checked in last year). Log it and move on. Do not silently fix adjacent security bugs during scoped work — silent fixes are unreviewable and often break callers. If it's serious, file a separate ticket.
UNVERIFIED: <assumption that has no source>
Emit when you rely on assumed-safe behavior you did not verify at the boundary: "this library sanitizes HTML" (does it? which version? which input?), "the upstream service strips control characters", "TLS is terminated at the proxy so this header is trusted". Blocks silent passage of guesswork.
ASK FIRST: <which Tier-2 situation you're about to enter>
Plan: <what you intend to do>
Emit when a change touches one of the Three-Tier "Ask First" situations (new auth flows, CORS changes, file upload endpoints, rate-limit adjustments, elevated-permission additions, new third-party integrations). Pause and surface the plan. These are the changes where a quiet mistake cascades.
The load-bearing artifact of this skill. Classify every security decision into one of three tiers: do reflexively, pause and ask, never do.
npm audit / equivalent before the slice lands.eval() or innerHTML with user data.Per-item rationale and exception notes live in references/boundary-system.md.
Validation belongs at the points where untrusted data enters trusted code, once, and nowhere else. Internal functions then assume their inputs are safe.
A trust boundary includes HTTP handlers (query, body, path, headers, cookies), form submissions, environment-variable loading, external service responses, file uploads, WebSocket messages, and anything read from a queue, cache, or object store that originated outside your code. Third-party APIs return untrusted data even if the integration has been stable for years.
safeParse boundary patternconst result = UserInputSchema.safeParse(input);
if (!result.success) return { error: result.error.flatten() };
const validated = result.data;
Validate once, at the boundary, into a typed shape. Downstream code takes the typed value and stops re-validating. If the urge to re-validate inside an internal function surfaces, the boundary is probably in the wrong place.
The Ask First gate on new auth flows is there because this area is where subtle mistakes turn into account takeover.
crypto.timingSafeEqual or equivalent). String == leaks timing information.Secure, HttpOnly, SameSite=Lax (or Strict for pure first-party flows). Never store session tokens where client JS can read them.Any change that introduces a new auth method, IdP, or role model is ASK FIRST territory.
UNVERIFIED belongs on any "this is encrypted in transit" claim that was not observed in config.
The review-time kramme:injection-reviewer agent catches these at PR stage; this section prevents them in the first draft.
Read references/owasp-top-10.md when a slice touches injection, XSS, parser, authentication, access-control, dependency, logging, or security-misconfiguration risk; it maps the OWASP categories to author-time prevention patterns.
"SELECT * FROM users WHERE id = " + userId. If the ORM exposes a raw-interpolation escape hatch, that's a code smell; the one good reason is usually not present.spawn(cmd, [arg1, arg2])), never shell-interpreted strings, never shell: true.textContent over innerHTML. Framework-specific: no dangerouslySetInnerHTML, v-html, [innerHTML] with user data.eval, no Function("..."), no setTimeout("...") with a string body, no dynamic import of user-controlled paths.Escape at output, validate at input, and the two disciplines compose safely.
Uploads are Ask First territory by default.
Don't trust the file extension — check magic bytes if critical.
A .jpg extension on a PHP file is a five-second attack. If the upload feeds into any content-sniffing path (serving, thumbnailing, AV scanning, executing), the MIME decision must come from the file's bytes, not its filename. Additionally: enforce a size cap, strip EXIF for user-uploaded images, store outside the web root, and generate server-side filenames (never echo the user's).
Starting point when no project-specific guidance exists:
Auth endpoints are tighter because they're the target of credential-stuffing and enumeration. Tune down further (e.g. 5 / 15 min) if the endpoint is high-value and low-traffic. Adjusting existing rate limits is ASK FIRST.
Run before every push when working near credential-handling code:
git diff --cached | grep -i "password\|secret\|api_key\|token"
Noisy on purpose — false positives are preferable to a real key landing in git history. Add project-specific patterns (PRIVATE_KEY, vendor prefixes) as the codebase warrants. A pre-commit hook that runs this automatically is a reasonable follow-up; treat that as a separate change.
Math.random().kramme:code:api-design owns where the trust boundary lives for a given surface — this skill owns what happens at that boundary. When adding a new endpoint, design the contract with kramme:code:api-design, then harden it here.kramme:code:incremental — each security-relevant change follows the slice discipline. Splitting a "fix auth + add rate limit + rotate the secret" change into three slices keeps each reviewable.kramme:auth-reviewer — verifies auth/authz/CSRF/session checks this skill was supposed to put in place.kramme:data-reviewer — verifies crypto usage, info-disclosure, and DoS bounds.kramme:injection-reviewer — verifies injection/XSS defenses at input→sink paths.A finding from any of the three agents that traces back to code authored with this skill applied is a signal that a rule above was skipped or misapplied — close the loop by updating this skill.
Lies you will tell yourself to skip security discipline. Each one has a correct response.
| Rationalization | Reality |
| --- | --- |
| "Internal tools don't need security." | Attackers target the weak link in a chain. |
| "We'll add security later." | Retrofitting is 10× harder. |
| "Just a prototype." | Prototypes become production. |
| "The framework handles it." | Maybe on the default path, not the one you're adding. Emit UNVERIFIED and check the docs for the version in use. |
| "The client already validates this." | Client-side validation is a UX feature. The server must validate independently — otherwise the API is a direct-write. |
| "It's behind a VPN, so it's safe." | Defense in depth. Every layer assumes the one in front of it has been bypassed. |
| "Logging the request body will help debug." | Until it logs a password. Redact before emitting; don't rely on a log processor. |
| "We'll rotate the secret once we're live." | The rotation path is the security control. Ship it on day one. |
| "I'll put the token in localStorage, it's easier." | Any XSS becomes account takeover. Use HttpOnly cookies. |
| "md5 is fine for this." | Probably not. State what "this" is out loud — if it's a security decision, use a modern hash. |
If any of these appear in your draft, stop and re-author:
md5, sha1, raw sha256, or plaintext.localStorage, sessionStorage, or any client-readable cookie.Access-Control-Allow-Origin: * on an endpoint that reads or mutates user data.ASK FIRST surfacing.safeParse (or equivalent) at the boundary.Before declaring a security-sensitive slice done, confirm every box. The extended version with per-item rationale and per-area grouping lives in references/security-checklist.md.
SIMPLICITY CHECK emitted — the security measure matches the threat, not imagined threats.innerHTML / dangerouslySetInnerHTML / v-html / eval / exec / Function(...) with user-derived data.Secure + HttpOnly + SameSite.git diff --cached | grep -i "password\|secret\|api_key\|token" is clean.npm audit / equivalent introduces no new high-or-critical findings.ASK FIRST situation surfaced and confirmed before implementation.NOTICED BUT NOT TOUCHING observation logged (ticket or PR description), not silently fixed.UNVERIFIED assumption either verified or explicitly left open with owner.kramme:auth-reviewer, kramme:data-reviewer, or kramme:injection-reviewer flag anything? Run them against the diff before opening the PR.If any box is unchecked, the slice is not done. Fix the gap or split the slice.
development
Runs kramme:pr:code-review as a closeout review loop for local or PR branch changes before commit, ship, or final response. Use when the user asks for autoreview, second-model review, or a final code-review pass after non-trivial edits. Not for UX, visual, accessibility, or product review.
development
Guides topic-level understanding verification for a PR, branch, feature, document, spec, design decision, bug fix, or other concrete subject. Use when the user asks to confirm, quiz, drill, teach-and-check, or verify that they understand a topic. Maintains a topic-specific checklist artifact and requires demonstrated understanding before marking the topic complete. Not for ordinary explanations without verification, end-of-session summaries, or code/test correctness checks.
testing
Design a CI/CD pipeline with quality gates, a <10-minute budget, feature-flag lifecycle, and an exit checklist. Use when adding a new CI pipeline, changing gate configuration, or planning a rollout for a new service. Complementary to kramme:pr:fix-ci (which fixes failures in an existing pipeline). Covers gate ordering, secrets storage, branch protection, rollback mechanism, and staged-rollout guardrails — not a rollout-execution runbook.
tools
--- name: kramme:visual:demo-reel description: Capture local demo evidence for observable product behavior: screenshots, before/after image sets, browser reels, terminal recordings, and short GIF/video proof. Use when shipping UI changes, CLI features, or any change where PR reviewers would benefit from visual or behavioral evidence. argument-hint: "[what to capture] [--url <url>|auto] [--tier static|before-after|browser-reel|terminal-recording]" disable-model-invocation: true user-invocable: tr