Skip to content
Ayhan Sipahi Ayhan Sipahi

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. The gap between “works in isolation” and “ships to production” is not a model capability problem; it is a specification problem. 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.

The Hidden Cost of Unstructured AI Code Generation

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.

A Framework for Structured AI Development

SpecKit puts four ordered steps between the prompt and the generated code, with two optional ones around them:

Constitution

Specify

Clarify

Plan

Tasks

Implement

Review & Iterate

Project Principles Core Values Standards

User Stories Acceptance Criteria Requirements

Resolve Ambiguities Clarify Details Edge Cases

Tech Stack Architecture Constraints

Task Breakdown Dependencies Priorities

Code Implementation Testing Documentation

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.

Phase 1: Constitution (Optional) - Establish Project Principles

The constitution phase helps establish core principles and standards for your project before diving into specific requirements.

# 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"

Phase 2: Specify - Define What You’re Building

The specification phase forces you to articulate your requirements clearly before any code gets generated. This prevents the classic problem of AI tools making incorrect assumptions about your needs.

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

Phase 3: Clarify (Optional) - Resolve Underspecified Areas

The clarify phase helps resolve any ambiguities or underspecified areas in your requirements before moving to 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?"

Phase 4: Plan - Define Architecture and Technical Decisions

The planning phase establishes your technical stack and architectural constraints. This prevents AI tools from making arbitrary technology choices that don’t align with your existing systems.

/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

## Phase 5: Tasks - Break Down Implementation into Manageable Steps

The task phase creates a Jira-like development plan that AI can follow systematically. This prevents the overwhelming approach of trying to implement everything at once.

```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

Phase 6: Implement - Execute Tasks Systematically

The implementation phase executes tasks one at a time, allowing for review and iteration at each step. This prevents the accumulation of errors and ensures code quality throughout development.

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 Quality Difference: Before vs After SpecKit

Before SpecKit (Typical AI Generation)

// 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 SpecKit (Specification-Driven)

// 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.

Team Adoption Strategies

Gradual Implementation Approach

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

Creating Team Standards

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

Integration with Code Review Process

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 implementation matches specifications rather than 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. That cost is visible on day one, and the return shows up later, during review and maintenance, where it is harder to attribute.

Two conditions decide whether the trade pays off. The feature has to be complex enough that a wrong assumption is expensive to unwind, and the team has to keep the specification current once implementation starts. A CRUD endpoint with three fields does not need a constitution. An auth flow with token rotation, rate limits, and reset semantics does.

The workflow 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 they are cheaper to settle than in a pull request thread.

Advanced Techniques and Best Practices

Handling Changing Requirements

SpecKit accommodates requirement changes through its iterative structure:

# 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

Integration with Existing Tools

SpecKit works alongside your current development workflow:

// .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
    }
  }
};

Specification Patterns for Different Project Types

API Projects:

/specify "REST API for e-commerce platform with product catalog, inventory management, and order processing. Must handle 1000+ concurrent users with sub-200ms response times."

Frontend Applications:

/specify "React dashboard for sales analytics with real-time charts, data filtering, and export functionality. Must work on mobile devices and support dark mode."

CLI Tools:

/specify "Command-line tool for database migrations with rollback support, environment management, and audit logging. Must work across different database providers."

Common Pitfalls

Over-Specification Trap

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"

Specification Drift

Keep specifications updated as implementation evolves:

# Regular specification updates
/specify --review

# Updates specifications based on implementation learnings
# Maintains alignment between spec and reality

Tool Lock-in Concerns

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

The Future of AI-Assisted Development

SpecKit belongs to a wider move toward written contracts around AI code generation. The contract outlives the chat session, survives a model swap, and can be reviewed by someone who never saw the original prompt.

Key trends emerging from this approach:

Collaborative AI Development

  • AI tools that maintain project context across sessions
  • Specification-aware code generation
  • Integration with project management tools
  • Automated documentation generation

Quality-First AI Coding

  • Built-in testing requirements
  • Security consideration templates
  • Performance requirement tracking
  • Technical debt prevention

Team-Scale AI Integration

  • Shared specification libraries
  • Consistent AI tool configuration across teams
  • Knowledge transfer through specifications
  • Standardized AI-generated code patterns

Getting Started: Your First SpecKit Project

The fastest way to judge the workflow is a 30-minute run on something real:

Choose a Real Feature

Pick something you’ve been postponing due to complexity:

  • User authentication system
  • Real-time notification service
  • Data export functionality
  • API integration layer

Run Through All Four Phases

# 1. Install and initialize
uvx --from git+https://github.com/github/spec-kit.git specify init test-feature --ai claude

# 2. Create specification (10 minutes)
/specify "Your feature description with clear requirements"

# 3. Define technical plan (5 minutes)
/plan "Your tech stack and constraints"

# 4. Generate task breakdown (5 minutes)
/tasks

# 5. Implement first task (10 minutes)
# Execute the highest priority task following SpecKit guidance

Compare Results

  • Code quality: How does the structured approach compare to your usual AI-generated code?
  • Maintainability: Would you be comfortable shipping this to production?
  • Documentation: How complete is the generated documentation?
  • Testing: Are the testing requirements clear and actionable?

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 becomes ceremony, and ceremony is what makes teams abandon the practice after the second feature.

The other boundary is maintenance. A specification that no longer matches the code is worse than no specification at all, because reviewers still 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

Related posts