GitHub Spec Kit: A Guide to Spec-Driven AI Development
How GitHub's Spec Kit turns loose AI code generation into structured, maintainable output through a four-phase specify-plan-tasks-implement loop.
Code generated by AI tools often passes a local smoke test but fails the production bar: ambiguous interfaces, unverified assumptions about upstream data, missing error handling, and structure that reflects the prompt rather than the codebase’s conventions. A clear, machine-readable specification of the interface, inputs, invariants, and error modes closes most of that gap before the model generates a single line.
GitHub’s SpecKit is one implementation of that idea: a CLI plus a set of slash commands that put a written specification, a technical plan, and a task list in front of the model before it writes anything. What follows is the specification format, the specify-plan-tasks-implement loop, the handoff points between the spec, the tests, and the generated code, and the failure modes (spec-as-afterthought, over-specification, brittle acceptance criteria) that turn the practice into ceremony rather than signal.
What a Vague Prompt Leaves Out#
Jumping straight to implementation with a vague prompt works for a quick MVP. It stops working as soon as the output has to survive a code review:
// Typical AI-generated code without specifications
function processUserData(data: any): any {
// AI tries to guess what you want
const result = data.map((item: any) => {
if (item.type === 'user') {
return { ...item, processed: true };
}
return item;
});
return result;
}
Without clear specifications, AI tools make assumptions about requirements, architecture, and implementation details. Those assumptions stay invisible in the diff: data: any hides the shape contract, and the untouched return item branch hides every type nobody thought about.
The Loop SpecKit Puts Between Prompt and Code#
SpecKit puts four ordered steps between the prompt and the generated code, with two optional ones around them:
Each step narrows the model’s freedom before the next one starts, so the generated code answers to the project’s principles and standards instead of to the wording of a single prompt.
Project Principles with /constitution#
An optional first pass. Core principles and standards for the project, written down before any specific requirement.
# Establish project constitution
/constitution
# Example project constitution
"This authentication system should prioritize:
- Security over convenience
- Explicit error handling over silent failures
- Testable code over clever implementations
- Clear documentation over self-documenting code
- Progressive enhancement over cutting-edge features"
Writing the Specification#
This phase forces you to articulate requirements before any code gets generated, closing the gaps the tool would otherwise fill with its own assumptions.
Installation and Setup#
# One-time usage
uvx --from git+https://github.com/github/spec-kit.git specify init my-project
# Persistent installation (recommended)
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
# Configure for specific AI tools
specify init my-project --ai claude # For Claude Code
specify init my-project --ai copilot # For GitHub Copilot
specify init my-project --ai gemini # For Gemini CLI
# Initialize in existing project
specify init --here --ai claude
Creating Your First Specification#
# Start the specification process
/specify
# Example specification for a user authentication system
"Build a user authentication system for a Next.js application that supports:
- Email/password registration and login
- JWT token management with refresh tokens
- Password reset functionality via email
- Rate limiting for auth endpoints
- Integration with existing TypeScript codebase
- Must follow security best practices for production use"
SpecKit transforms this high-level description into detailed user stories and acceptance criteria:
## User Stories Generated by SpecKit
### Epic: User Authentication System
- **US-001**: As a new user, I want to register with email/password so I can access the application
- **US-002**: As a registered user, I want to login with my credentials so I can access protected features
- **US-003**: As a user, I want to reset my password if I forget it so I can regain access to my account
- **US-004**: As a system, I want to limit authentication attempts to prevent brute force attacks
### Acceptance Criteria
- Registration requires valid email format and password strength validation
- JWT tokens expire after configurable time period
- Refresh tokens enable seamless session extension
- Password reset emails expire after 15 minutes
- Rate limiting allows maximum 5 attempts per IP per minute
Resolving Ambiguity with /clarify#
The second optional step, run before technical planning:
# Clarify underspecified areas
/clarify
# SpecKit identifies and helps resolve ambiguous requirements
"How should the system handle concurrent login attempts from the same user?"
"What happens when a user tries to register with an email that's already in use?"
"Should password reset tokens be single-use or reusable?"
The Technical Plan#
Stack and architectural constraints go here. Without them, the tool has no reason to avoid a technology that clashes with what you already run.
/plan
# Define your technical constraints
"Tech stack: Next.js 15 with App Router, TypeScript, Prisma ORM with PostgreSQL
Architecture: Server actions for auth operations, middleware for route protection
Security: bcrypt for password hashing, jose for JWT handling
Email: Nodemailer with SMTP
Constraints: Must integrate with existing shadcn/ui components, follow current project structure"
SpecKit generates a comprehensive technical plan:
## Technical Architecture Plan
### Technology Stack
- **Frontend**: Next.js 15 App Router, TypeScript, shadcn/ui
- **Backend**: Next.js API routes, Server Actions
- **Database**: PostgreSQL with Prisma ORM
- **Authentication**: JWT with refresh tokens
- **Security**: bcrypt, jose, rate-limiting middleware
- **Email**: Nodemailer with SMTP configuration
### Database Schema
```sql
-- Users table
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Refresh tokens table
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Security Considerations#
- Password hashing with bcrypt (salt rounds: 12)
- JWT access tokens (15-minute expiry)
- Refresh tokens (7-day expiry)
- Rate limiting: 5 attempts per IP per minute
- CSRF protection via SameSite cookies
## The Task List
The output is a Jira-like plan the model can work through one item at a time.
```bash
/tasks
SpecKit generates numbered, prioritized tasks with clear dependencies:
## Implementation Tasks
### Setup and Configuration (Priority: High)
1. **Task 1**: Set up Prisma schema for users and refresh_tokens tables
- Dependencies: None
- Estimated time: 30 minutes
- Deliverable: Working database schema with migrations
2. **Task 2**: Create authentication utilities (password hashing, JWT handling)
- Dependencies: Task 1
- Estimated time: 45 minutes
- Deliverable: Utility functions for auth operations
3. **Task 3**: Implement rate limiting middleware
- Dependencies: None
- Estimated time: 30 minutes
- Deliverable: Middleware function with Redis/memory store
### Authentication Implementation (Priority: High)
4. **Task 4**: Build registration server action
- Dependencies: Task 1, Task 2
- Estimated time: 1 hour
- Deliverable: Registration endpoint with validation
5. **Task 5**: Build login server action with JWT generation
- Dependencies: Task 1, Task 2
- Estimated time: 1 hour
- Deliverable: Login endpoint returning access/refresh tokens
6. **Task 6**: Implement token refresh mechanism
- Dependencies: Task 2, Task 5
- Estimated time: 45 minutes
- Deliverable: Token refresh endpoint
### UI Components (Priority: Medium)
7. **Task 7**: Create login form with shadcn/ui components
- Dependencies: Task 5
- Estimated time: 1 hour
- Deliverable: Styled login form with validation
8. **Task 8**: Create registration form
- Dependencies: Task 4
- Estimated time: 1 hour
- Deliverable: Registration form with password strength validation
Implementing One Task at a Time#
Each task is executed, reviewed, and corrected before the next one starts. A mistake in Task 2 gets caught there, before it becomes the foundation for Tasks 4 through 8.
Task 1 Implementation Example#
// prisma/schema.prisma - Generated with SpecKit guidance
model User {
id String @id @default(cuid())
email String @unique
passwordHash String @map("password_hash")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
refreshTokens RefreshToken[]
@@map("users")
}
model RefreshToken {
id String @id @default(cuid())
userId String @map("user_id")
token String @unique
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("refresh_tokens")
}
Task 2 Implementation Example#
// lib/auth.ts - Utility functions with proper error handling
import bcrypt from 'bcryptjs';
import { SignJWT, jwtVerify } from 'jose';
export class AuthError extends Error {
constructor(message: string, public code: string) {
super(message);
this.name = 'AuthError';
}
}
export async function hashPassword(password: string): Promise<string> {
try {
return await bcrypt.hash(password, 12);
} catch (error) {
throw new AuthError('Failed to hash password', 'HASH_ERROR');
}
}
export async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
try {
return await bcrypt.compare(password, hash);
} catch (error) {
throw new AuthError('Failed to verify password', 'VERIFY_ERROR');
}
}
export async function generateAccessToken(
payload: { userId: string; email: string }
): Promise<string> {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('15m')
.setIssuedAt()
.sign(secret);
}
export async function verifyAccessToken(
token: string
): Promise<{ userId: string; email: string }> {
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const { payload } = await jwtVerify(token, secret);
return payload as { userId: string; email: string };
} catch (error) {
throw new AuthError('Invalid token', 'TOKEN_INVALID');
}
}
The Same Registration Handler, Twice#
Before#
// Unstructured AI-generated auth
export default async function handler(req: any, res: any) {
if (req.method === 'POST') {
const { email, password } = req.body;
// Hash password somehow
const hash = bcrypt.hashSync(password, 10);
// Save to database
const user = await prisma.user.create({
data: { email, password: hash }
});
// Return something
res.json({ success: true });
}
}
After#
// server/auth/register.ts - Structured, maintainable implementation
import { z } from 'zod';
import { ratelimit } from '@/lib/rate-limit';
import { hashPassword } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
const registerSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'Password must contain uppercase, lowercase, and number')
});
export async function registerUser(formData: FormData) {
// Rate limiting
const { success } = await ratelimit.limit('auth_register');
if (!success) {
throw new Error('Too many registration attempts. Please try again later.');
}
// Input validation
const result = registerSchema.safeParse({
email: formData.get('email'),
password: formData.get('password')
});
if (!result.success) {
throw new Error(`Validation failed: ${result.error.issues.map(i => i.message).join(', ')}`);
}
const { email, password } = result.data;
try {
// Check if user exists
const existingUser = await prisma.user.findUnique({
where: { email }
});
if (existingUser) {
throw new Error('User already exists with this email');
}
// Create user with hashed password
const passwordHash = await hashPassword(password);
const user = await prisma.user.create({
data: {
email,
passwordHash
},
select: {
id: true,
email: true,
createdAt: true
}
});
return { success: true, user };
} catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error('Registration failed. Please try again.');
}
}
The second version adds schema validation, a duplicate-email check, rate limiting, and typed error paths. Each of those traces back to a line in the acceptance criteria, so all of them were written before any reviewer looked at the diff.
Bringing It to a Team#
Starting with One Feature#
Start with a single feature rather than a team-wide mandate:
# Step 1: Choose a complex feature that's been postponed
specify init payment-processing --ai claude
# Step 2: Complete the feature using all four phases
# Record where the specification changed the output
# Step 3: Walk the team through the spec and the diff side by side
# Compare review comments against a recent feature of similar size
Specification Templates#
Establish specification templates for common project types:
## API Endpoint Specification Template
### Functional Requirements
- [ ] Define input parameters and validation rules
- [ ] Specify output format and error responses
- [ ] Document authentication/authorization requirements
- [ ] Define rate limiting and caching strategies
### Technical Requirements
- [ ] Specify database interactions and queries
- [ ] Define logging and monitoring requirements
- [ ] Document integration points with external services
- [ ] Specify testing requirements (unit, integration, e2e)
### Performance Requirements
- [ ] Response time targets
- [ ] Concurrent user handling
- [ ] Database query optimization requirements
- [ ] Caching strategy and invalidation rules
Team Configuration#
A checked-in config file keeps the tool aligned with the conventions the team already uses:
// .speckit/config.js - Team configuration
export default {
aiTool: 'claude',
projectStructure: {
srcDir: 'src',
testDir: '__tests__',
docsDir: 'docs'
},
codeStandards: {
formatter: 'prettier',
linter: 'eslint',
testing: 'jest'
},
integrations: {
jira: {
enabled: true,
projectKey: 'AUTH'
},
github: {
issueTemplates: true,
prTemplates: true
}
}
};
What Reviewers Get#
SpecKit-generated code simplifies code reviews by providing context:
// Each implementation includes specification reference
/**
* Task 4: Registration server action
* Specification: US-001 - User registration with email/password
* Dependencies: Task 1 (database schema), Task 2 (auth utilities)
* Security requirements: Password strength validation, rate limiting
*/
export async function registerUser(formData: FormData) {
// Implementation follows specification exactly
}
Reviewers can verify that the implementation matches the specification instead of guessing at requirements.
What Specification Work Costs#
The workflow moves effort earlier; it does not remove it. Writing the specification, resolving the ambiguities, and reviewing a task list all consume time while no code exists yet, and the return on that time shows up later, during review and maintenance.
It also changes what a code review argues about. A reviewer compares the implementation against a written acceptance criterion instead of reconstructing intent from the diff, and disagreements move to the specification.
Where Specifications Go Stale#
Requirements move. An update goes in through the same entry point as the original spec, and SpecKit reports what the change affects downstream:
# When requirements change mid-development
/specify --update
# Provide the new requirements
"Add OAuth integration with Google and GitHub to the existing auth system"
# SpecKit updates specifications and regenerates affected tasks
# Shows impact analysis of changes on existing implementation
The quieter failure is that nobody runs that command, and the spec drifts until it no longer describes the code. A review pass keeps the two aligned:
# Regular specification updates
/specify --review
# Updates specifications based on implementation learnings
# Maintains alignment between spec and reality
Over-Specifying the Implementation#
Don’t specify every implementation detail. Focus on requirements and constraints:
Bad: "Use a for loop to iterate through the array and apply a filter function"
Good: "Filter user data to show only active users from the last 30 days"
Portability Between AI Tools#
SpecKit generates standard documentation that works with any AI tool:
# Generated specifications are tool-agnostic
## Requirements Document
- Compatible with Claude Code, GitHub Copilot, Gemini
- Standard markdown format
- Clear acceptance criteria
- Portable between different AI coding tools
When the Four-Phase Loop Is Worth It#
The loop earns its overhead when a feature has enough surface area that a wrong assumption costs more than the specification did: auth flows, billing, data migrations, anything whose error modes only appear under load. For a one-file change or a throwaway script, skip it; the specification turns into ceremony there.
The other boundary is maintenance. A specification that no longer matches the code can still mislead reviewers who trust it. If requirements churn faster than anyone will update the spec, keep the plan phase for the architectural decisions and drop the rest.
A reasonable next step is one postponed feature, run through all four phases, compared against what the same prompt produces on its own.
References#
- GitHub Spec Kit Repository (opens in new tab) - Official GitHub repository for the Spec Kit toolkit, including templates and the spec-driven development workflow.
- GitHub Spec Kit Documentation (opens in new tab) - Official documentation site for Spec Kit, covering the Specify-Plan-Implement workflow and integration with AI coding agents.
- Spec-Driven Development Guide (opens in new tab) - The core guide to specification-driven development principles in the GitHub Spec Kit methodology.
- GitHub Copilot Documentation (opens in new tab) - Official documentation for GitHub Copilot, relevant to understanding AI-assisted code generation in the SpecKit context.
- Research: How GitHub Copilot Helps Improve Developer Productivity (opens in new tab) - GitHub research on Copilot productivity impact, providing context for specification-driven approaches to AI code quality.
Related posts
A practical repo layout that keeps Claude Code, Codex, Copilot, Cursor, and OpenCode reading the same rules, with honest notes on where portability breaks.
ai-tools · claude-code · github-copilot +3
A framework for six levels of AI assistance in software, from code review to vibe coding, with guidance on when to dial AI help up or down.
ai-tools · code-quality · productivity +4
A plan's job is to pre-decide what the agent would otherwise decide silently. The document skeleton that does it, traced through one shipped change.
claude-code · ai-agents · documentation +2
When a coding agent underperforms, the reflex is a stronger model. On bounded tasks the harness moves the score at least as much; a rule for which lever to pull.
ai-agents · ai-tools · llm +3
Devcontainers, Codespaces and AWS Lambda MicroVMs as homes for a coding agent: what each rung adds, what it costs, and when moving the agent off the laptop pays off.
lambda · claude-code · ai-tools +5