Spec it safe: shift left and harden your specs with security requirements
Rajdeep Mukherjee
Applied Science
Debashis Das
Information Security
Spec-driven development (SDD) is changing how large teams build software. Instead of relying on a prompt alone, you author a specification that guides an AI coding agent through design, tasks, and implementation; a flow we abbreviate here as Specify → Plan → Execute. But most specs describe what the software should do, the features a user can see and exercise, and don’t mention how it should withstand misuse. Functional requirements are naturally self-demonstrating: if login is broken, someone notices. Security requirements are often not; an app can pass every functional check while leaving an injection or authorization flaw wide open, because nothing exercises the failure case. So when a security constraint is missing from the spec, the agent has no security-focused target to build and iterate against. It falls back on whatever the framework defaults happen to do, which may be sufficient or may leave a hole, and nothing in the spec forces the question either way.
Closing that gap isn’t about adding a better model or tacking on a security review at the end. It’s about moving the work earlier, to a layer you fully control: the specification. When you write a security obligation into the spec, the coding agent has something concrete to build toward and iterate against, instead of leaving it for a reviewer to catch after the code already exists. That’s the shift from treating security as an afterthought, bolted on once the design is settled, to designing for it from the start. It typically pays off in the architecture too. Stating a security requirement early can point you toward a cleaner design, one where meeting the requirement is simpler because you built around it rather than retrofitting later. Specification quality is something you can shape before any code exists, and that is what specification hardening was designed to do.
We introduce a Kiro Power for hardening specifications, a plugin that adds one extra step to the SDD workflow. After your functional requirements are written and before planning starts, it reads your EARS requirements, flags the types of weaknesses their wording hints at, and proposes security obligations you can trace and test. Those proposals come from a curated library that maps specific CWE weakness classes to reusable security obligations, so what specification hardening suggests is based on a known weakness rather than a model’s guess. Relevance comes from the requirements themselves: specification hardening proposes an obligation only when your initial wording points to the weakness it addresses, such as user input, authentication, or data handling, so you see constraints that fit what the application does. Security goes into the spec itself, not into a later code review, although we highly recommend you include a security review step after too.

Specification hardening is an upstream control point, not a runtime security boundary. It reduces the chance that a common weakness class is omitted and makes security intent explicit, traceable, and testable. It does not verify the generated code, prove the absence of vulnerabilities, or replace SAST, DAST, dependency scanning, or human review.
What specification hardening targets: weakness classes that are both expressible as a requirement and detectable from EARS wording: injection, missing authentication, weak credential storage, unencoded output, and similar omissions.
What specification hardening does not target: weaknesses that depend on data-flow or code-level analysis, authorization gaps that are not lexically signaled, and anything about the runtime, infrastructure, or dependencies. Those still need a human reviewer and downstream tooling.
Not every weakness can be fixed in a spec; many are architectural or only visible in code. But a selected set of requirements-expressible CWE classes can be mapped to reusable security obligations that mitigate the common form of each weakness when correctly implemented and verified. Specification hardening ships these mappings as a deterministic TypeScript library and CLI, so every constraint is reproducible.
| CWE | Weakness | Trigger condition | Required EARS constraint |
|---|---|---|---|
| CWE-89 | SQL Injection | Requirement queries a database using external input | Execute all queries via parameterized / prepared statements only; never concatenate. |
| CWE-862 | Missing Authorization | Returns or mutates a resource owned by a principal | Verify the requesting principal owns the resource or holds an explicit grant. |
| CWE-306 | Missing Authentication | Exposes an endpoint or resource | Require a valid authenticated session or signed token before processing. |
| CWE-79 | Cross-Site Scripting | Renders user-supplied content in a browser | Context-encode all output; add a Content Security Policy header as defense in depth. |
| CWE-522 | Insufficiently Protected Credentials | Stores or verifies passwords | Hash passwords with a strong adaptive function (never plaintext or reversible). |
| CWE-434 | Unrestricted File Upload | Accepts file uploads | Validate magic bytes against a MIME allowlist; cap size; store outside web root. |
| CWE-639 | IDOR / BOLA | Fetches an object by caller-supplied identifier | Verify the object’s owning tenant matches the authenticated caller before returning. |
The library ships 60 CWE mappings across eight security domains (injection, authentication, authorization, data exposure, cryptography, input validation, security misconfiguration, and business logic). The weaknesses come from MITRE’s Common Weakness Enumeration (CWE); their selection and detector patterns draw chiefly on the OWASP Top 10:2021 and the Amazon Q Developer Detector Library (formerly CodeGuru). Each mapping carries an OWASP category and a manually assigned severity. The constraints in the table are representative paraphrases, not complete cross-architecture controls.
Specification hardening runs a three-stage pipeline over each requirements file. The engine is deterministic: with the same tool version, mappings, options and unhardened input, the same spec always gets the same hardening. The agent’s semantic review that follows (Figure 2) is not deterministic.
The engine matches each requirement against 60 CWE mappings using two lexical signals: substring keywords (“database”, “upload”, “session”) and regex patterns. It combines the hits into a bounded score, and a CWE is flagged when the score clears a threshold (0.3 by default). Matching is lexical, not semantic, so it can miss weaknesses the wording doesn’t explicitly describe.
For each flagged CWE, the engine checks whether the spec already states the needed primitive, such as a parameterized-query clause, an ownership check or output encoding. Only uncovered gaps produce proposals. The check is file-wide and lexical, so a control stated for one requirement can suppress a proposal for another.
Instead of blocking, the engine inserts each missing constraint as an EARS item. It fills placeholders from the source requirement where it can and falls back to generic wording (“a user”, “the resource”) when it can’t. Each inserted line includes its proposal ID and a [SECURITY: …] tag, so the author can review the changes as a diff. Some mappings layer controls: CWE-89 adds both a parameterized-query clause and an input-validation clause. The programmatic API returns a structured report with each constraint’s CWE, priority and rationale. The CLI writes the constraints in place and doesn’t save that report.
Specification hardening reads only the spec, not the code. It proposes the CWE-derived constraints that the requirement text signals, and the author confirms them against the running system. Making those invariants explicit makes them easier to preserve and test.
![Workflow diagram in two bands. The top purple band, ‘Specification Hardening Engine · Kiro Power that runs over requirements.md’, flows left to right: Requirements (requirements.md in EARS, with a dashed ‘review marker’ box noting a re-run hardens only new or changed requirements), Detect CWE triggers (keywords + regex patterns vs. 61 CWE mappings, score ≥ 0.3 means the CWE applies), Close coverage gaps (checks each CWE constraint against the whole spec and writes missing ones as EARS in the requirement’s terms), and Hardened spec (edited in place, [SECURITY:] items nested plus a Security Baseline). A ‘weak signal’ arrow drops from Detect CWE triggers into the bottom orange band, ‘Agent semantic review’, which notes that lexical matching can’t see a missing ownership or tenancy check and flows through Review candidates (weak hits scoring 0.05–0.3 on object-level authorization CWEs such as CWE-639/862), Boundary sweep (is caller-to-resource access constrained on every principal-owned resource?), and Add constraint (each confirmed gap becomes a [SECURITY:] EARS item in the domain’s terms, such as ‘active care relationship’), which feeds back up into Hardened spec. A dashed loop across the top reads ‘Spec changed → re-run on the same file (only new/changed requirements hardened)’.](/images/blogs/spec-it-safe/hardening-engine-workflow.png?h=4afaaf98)
The easiest way to see why this matters is to watch the same feature ship twice: once from a functionality-only spec, once from a spec hardened by the specification hardening engine. A digital-health startup is building a patient portal where a physician looks up clinical notes by patient name. The product manager writes the requirement most would write:
Requirement: minimally functional and security-silent (EARS, event-driven)
It reads as complete, but it is only minimally functional. It says nothing about matching semantics, pagination, empty results, which fields may be returned, or who may run it. A coding agent could produce an Express.js endpoint like this (illustrative pseudocode):
Without explicit security constraints (illustrative pseudocode)
Two weaknesses are embedded here. The string interpolation in the SQL clause is textbook CWE-89: SQL Injection (an input such as %' OR '1'='1' -- adds an always-true clause and comments out the rest, returning every row; exact syntax varies by database). And the endpoint has no authentication check at all, which is CWE-306, Missing Authentication for Critical Function. A third, deeper issue, whether an authenticated clinician should see every patient’s notes, comes up later. The omission affects the requirements, and the implementation wasn’t defensive by default.
Now run the same requirement through the specification hardening engine. Its lexical detector flags two weakness classes (CWE-89 SQL Injection and CWE-306 Missing Authentication) and writes seven sub-numbered EARS constraints back into the requirement, each with a traceable proposal ID and a [SECURITY: …] tag, as shown below. A clause proposed under three or more requirements is moved once into a shared ## Security Baseline (Cross-Cutting) section; this is a heuristic, so check it in the diff.
Specification hardening output after review, written back into requirements.md. The engine adds seven obligations across two CWEs: 1.1–1.2 for CWE-89 (parameterized-query), 1.3–1.4 for CWE-89 (input-validation), and 1.5–1.7 for CWE-306 (authentication). The review pass, done by the Kiro agent in the Power or by you on the CLI, then adds 1.8 (data-minimization, CWE-200) and 1.9 (care-relationship authorization, CWE-862). Representative clauses are shown; the rest are omitted for brevity. The programmatic API also returns a structured report with each engine constraint’s CWE, priority, and rationale; the CLI does not persist that report.
Reading the IDs: SEC-REQUIREMENTS-REQ-1-EC-89-1-1 reads left to right as SEC (a security constraint), REQUIREMENTS-REQ-1 (the first requirement in requirements.md; in a spec with “Requirement N” headings this becomes REQ-1.1, criterion 1 of Requirement 1), and EC-89-1 (the first EARS constraint template in the CWE-89 mapping). The trailing 1 numbers the clause and appears only when one template expands into several EARS lines, which is why the single-clause EC-306-1 has no suffix. Clauses added in review use SEM in place of EC, e.g. SEM-862-1.
A developer reviews the hardened spec diff and uses judgment on each clause. The engine’s clauses cover injection and authentication, but nothing in the wording told it to limit the returned fields, so the review adds 1.8 (running with --semantic-all also lists CWE-200 as a candidate). Once the spec looks good, the agent generates code from it, building against explicit obligations instead of guessing. This candidate gates the endpoint behind a fail-closed 401 (1.5–1.7), validates the search term (1.3), binds it as a parameter with an explicit LIKE escape (1.1–1.2), and returns only the fields 1.8 names:
Conformance draft: satisfies the parameterized-query (1.1–1.2), input-validation (1.3), authentication (1.5–1.7), and data-minimization (1.8) clauses, but it is NOT yet safe for real patient data: it does not yet enforce 1.9.
Just as important is what the tool flagged but did not write. Any authenticated physician can read any patient’s notes, because there is no care-relationship check: a missing authorization check (CWE-862) that scores below the threshold. Instead of writing it, the tool surfaces it as a semantic candidate for the reviewer to confirm. In the Kiro Power that reviewer is the agent; on the CLI it is you. The confirmed gap becomes clause 1.9, specialized to the domain: “return clinical notes only for patients with whom the requesting physician has an active care relationship”. The query then becomes authorization-scoped:
Authorization-scoped query: the parameterized, authenticated read plus the review-added care-relationship predicate (1.9, CWE-862). It assumes a PostgreSQL locale/collation with the required ILIKE behavior; schema and policy are illustrative.
Deterministic hardening surfaced seven obligations across two CWEs here. The data-minimization rule and the domain-specific authorization rule still came from review.
Specification hardening augments security judgment, it does not replace it.
Specification hardening is packaged as a Kiro Power, available in the sample-kiro-spec-security-hardening repository on GitHub. You can run it two ways.
Through the Kiro Power. Once your functional requirements are final, ask Kiro to “harden my specs”. The harden-spec skill:
- finds the bundled engine and builds it on first use;
- runs it over your
.kiro/specs/*/requirements.md; - does the semantic review pass for missing authorization checks that keyword matching can’t see;
- summarizes the constraints it added and any warnings that need your attention.
From the CLI. Build once with npm install in cwe-to-spec-mapping-library/. Then point the compiled CLI at a spec file or a specs directory, using absolute paths:
The --semantic-candidates flag prints weak authorization matches for review but doesn’t write them to the file.
Both paths edit requirements.md in place. That means hardening fits into version control and code review. The engine only adds lines; it never changes, splits or reorders a requirement, and it reports requirements that aren’t strict EARS instead of rewriting them. Always review the git diff. Re-running is incremental: a hidden review marker records the requirements already hardened, so specification hardening hardens only new or changed ones and doesn’t re-propose constraints a reviewer removed. Run it on the spec as it is; you never need a pre-hardening copy.



Because specification hardening operates on the spec rather than the code, the same hardening step slots into three kinds of work. They differ only in where the requirements come from and when specification hardening runs:
Greenfield (new projects). Hardening runs as a step in the spec workflow, before any code is written: given the EARS requirements, specification hardening adds the missing [SECURITY: …] constraints, and the agent implements against the hardened spec. Security intent is present in the spec from the first commit rather than retrofitted later.
Brownfield (existing code). When a requirement is added or changed, re-run specification hardening on the existing spec. It hardens only the new or changed requirements and leaves the ones already reviewed untouched. The added SEC- constraints show up as a reviewable diff alongside the feature work, so the change stays scoped to what was modified. Because specification hardening reads the spec, not the code, check each proposed obligation against how the existing system actually behaves. Where they differ, you’ve found either a gap in the code or a constraint to adapt.
Spec-driven modernization (legacy rewrites). Requirements are first extracted from the legacy system into EARS specs, for example with AWS Transform; specification hardening then batch-hardens that recovered set before re-implementation. The modernized system is rebuilt to an explicitly hardened specification instead of inheriting the original gaps.

Specification hardening is an early step into a fairly new area: securing the specification itself and using it to steer coding agents toward secure code by design. We think the spec is the right place to start, because it’s the earliest artifact both people and agents work from. But specification hardening is a research prototype and a hypothesis, and the blog makes the case for spec-level controls.
Spec hardening also isn’t the only way to get secure code out of an agent, and it works best alongside the others. Threat modeling catches design-level risks that keyword matches can miss. Secure coding guidance in the agent’s context, such as Kiro steering files, shapes code as it’s written. Frameworks with safe defaults remove whole classes of mistakes. Downstream, SAST, DAST, dependency scanning, AI-aided code security scanning (such as provided by AWS Continuum), security tests and human review still check what was built. Specification hardening adds one more layer, at the point where intent gets written down.
The hardened spec stays in the repository, so the reasoning behind each obligation is still there to audit after the tasks run. If your team is adopting SDD, add a spec security review to your definition of done. Agents can build security in from the start, but only if we tell them what that means.