Module 8: Capstone project: Your first project with Claude Code

Tests, Commit, and Deliver: Closing the Project

Tests, Commit, and Deliver: Closing the Project

Project goal

Build a working CLI tool using the complete Claude Code workflow. In this capsule you complete Phase 4: Test, Commit, Deliver — you'll generate tests, review the code, commit, and close the loop on the capstone project.


What you built in previous modules

  • Capsule 02: The complete setup (CLAUDE.md, skills, hooks)
  • Capsule 03: Explore + the implementation plan
  • Capsule 04: 3 commands implemented and working

Your CLI works. Now it needs to be validated, versioned, and delivered.


What you'll add in this module

By the end of this capsule you'll have:

  • Tests for the 3 commands (generated by Claude Code)
  • Reviewed, clean code
  • A commit with a descriptive message
  • A PR created (optional, if you use GitHub)
  • A complete, evaluable project

Step-by-step walkthrough

Step 1: Generate tests with Claude Code

Use the /add-tests skill you created in Capsule 02. This skill tells Claude exactly how to generate tests for your commands.

Generate tests for the first command

/add-tests src/commands/add.py

Generate tests for the add command. Include:
- Test adding a note with basic text
- Test adding a note with a tag
- Test adding a note without a tag (tag must be None)
- Test empty text (it must fail or handle the case)

Claude reads the .claude/skills/add-tests/SKILL.md skill, analyzes the command, and generates the test file.

Expected output from Claude:

I created tests/test_add.py with 4 tests:
- test_add_basic: adds a note with simple text
- test_add_with_tag: adds a note with a tag
- test_add_without_tag: verifies tag is None by default
- test_add_empty_text: verifies empty text handling

Running: pytest tests/test_add.py -v

Expected pytest output:

tests/test_add.py::TestAdd::test_add_basic PASSED
tests/test_add.py::TestAdd::test_add_with_tag PASSED
tests/test_add.py::TestAdd::test_add_without_tag PASSED
tests/test_add.py::TestAdd::test_add_empty_text PASSED

4 passed in 0.12s

Example of a generated test (Python)

Claude should generate something similar to this:

"""Tests for the add command."""

import json
from pathlib import Path

from click.testing import CliRunner

from src.cli import cli


class TestAdd:
    """Tests for the add command."""

    def setup_method(self) -> None:
        self.runner = CliRunner()

    def test_add_basic(self, tmp_path: Path) -> None:
        """Adds a note with basic text."""
        with self.runner.isolated_filesystem(temp_dir=tmp_path):
            result = self.runner.invoke(cli, ["add", "My test note"])
            assert result.exit_code == 0
            assert "#1" in result.output

    def test_add_with_tag(self, tmp_path: Path) -> None:
        """Adds a note with a tag."""
        with self.runner.isolated_filesystem(temp_dir=tmp_path):
            result = self.runner.invoke(
                cli, ["add", "Note with a tag", "--tag", "work"]
            )
            assert result.exit_code == 0

            notes_file = Path(".notes.json")
            data = json.loads(notes_file.read_text())
            assert data["notes"][0]["tag"] == "work"

    def test_add_without_tag(self, tmp_path: Path) -> None:
        """Verifies that tag is None by default."""
        with self.runner.isolated_filesystem(temp_dir=tmp_path):
            result = self.runner.invoke(cli, ["add", "Note without a tag"])
            assert result.exit_code == 0

            notes_file = Path(".notes.json")
            data = json.loads(notes_file.read_text())
            assert data["notes"][0]["tag"] is None

    def test_add_empty_text(self, tmp_path: Path) -> None:
        """Empty text handling."""
        with self.runner.isolated_filesystem(temp_dir=tmp_path):
            result = self.runner.invoke(cli, ["add", ""])
            assert result.exit_code != 0 or "empty" in result.output.lower()

Generate tests for the other commands

Repeat the process for list and search:

/add-tests src/commands/list_notes.py

Generate tests for the list command. Include:
- Test listing with no notes (empty list)
- Test listing with several notes
- Test filtering by tag with --tag
- Test JSON output with --json
/add-tests src/commands/search.py

Generate tests for the search command. Include:
- Test a search with results
- Test a search with no results
- Test a case-insensitive search
- Test a search with partial text

Run all the tests

Once you've generated tests for all 3 commands:

Run the whole test suite with verbose output:
pytest -v

Expected output:

tests/test_add.py::TestAdd::test_add_basic PASSED
tests/test_add.py::TestAdd::test_add_with_tag PASSED
tests/test_add.py::TestAdd::test_add_without_tag PASSED
tests/test_add.py::TestAdd::test_add_empty_text PASSED
tests/test_list.py::TestList::test_list_empty PASSED
tests/test_list.py::TestList::test_list_with_notes PASSED
tests/test_list.py::TestList::test_list_filter_tag PASSED
tests/test_list.py::TestList::test_list_json_output PASSED
tests/test_search.py::TestSearch::test_search_with_results PASSED
tests/test_search.py::TestSearch::test_search_no_results PASSED
tests/test_search.py::TestSearch::test_search_case_insensitive PASSED
tests/test_search.py::TestSearch::test_search_partial PASSED

12 passed in 0.35s

Handling failing tests

If a test fails, Claude will see it in the output and should offer fixes:

tests/test_search.py::TestSearch::test_search_case_insensitive FAILED

FAILED tests/test_search.py::TestSearch::test_search_case_insensitive
- AssertionError: assert 0 == 0
  E  assert 1 == 0

Ask Claude to fix it:

The test_search_case_insensitive test failed. Investigate why
and fix it. It could be a bug in the code or in the test.

Claude analyzes the error, works out whether the problem is in the implementation or in the test, and fixes it. Then run it again:

Run the test that failed to confirm it's fixed:
pytest tests/test_search.py::TestSearch::test_search_case_insensitive -v

Step 2: Review the code

With the tests passing, ask Claude for a full review of the code.

The review prompt

Review all the code we wrote. Analyze:
1. Are there code smells or anti-patterns?
2. Is the error handling consistent?
3. Are CLAUDE.md's conventions respected?
4. Is there dead code or unused imports?
5. Are the docstrings complete?
6. Are there improvements worth making?

Just report. Don't change anything yet.

Typical review output

## Code review: notes-cli

### Files reviewed
- src/cli.py ✅
- src/utils.py ✅
- src/commands/add.py ✅
- src/commands/list_notes.py ⚠️
- src/commands/search.py ✅

### Findings

#### 🟡 Suggestion: list_notes.py
The format_notes_table function could be pulled out into utils.py
since search.py has a similar function. It would avoid duplication.

#### 🟢 Done well
- Consistent error handling with click.ClickException
- Type hints on every public function
- Docstrings present on every command function
- Storage manager centralized in utils.py
- Consistent snake_case per CLAUDE.md

#### ✅ Not found
- No dead code
- No unused imports
- No obvious anti-patterns

### Recommendation
The code is clean and functional. The suggestion to extract
format_notes_table is a minor improvement — only do it
if you have time.

Verdict: ✅ Ready to commit

Implement improvements (optional)

If Claude found improvements worth making:

Implement the suggestion to extract format_notes_table into utils.py.
Update list_notes.py and search.py to use it from utils.
Then run the tests to check we didn't break anything.

Claude refactors, and the tests confirm everything still works.


Step 3: Update CLAUDE.md

Now that the project is complete, update CLAUDE.md to reflect the final state:

Update CLAUDE.md to reflect the real commands
(add, list, search) instead of the placeholders
(command_one, command_two, command_three). Don't change the
conventions or the rules — just the structure and the
command names.

Claude updates the structure section:

## Structure
src/
├── __init__.py
├── cli.py              → Entry point, Click command group
├── commands/
│   ├── __init__.py
│   ├── add.py           → Add a note with text and a tag
│   ├── list_notes.py    → List notes with filters
│   └── search.py        → Search notes by text
└── utils.py             → JSON storage and output formatting

tests/
├── __init__.py
├── test_add.py
├── test_list.py
└── test_search.py

Step 4: Git commit

This is the moment the PreToolUse hook comes into play. When Claude tries to run git commit, the hook will run the tests automatically.

First commit: the complete project

Create a commit with all the project's files.
Use a descriptive message explaining what we built.

Claude runs:

git add .
git commit -m "feat: build notes-cli with 3 commands (add, list, search)"

But before the commit, the hook fires:

[Hook: PreToolUse] Detected: git commit
[Hook: PreToolUse] Running: pytest --tb=short -q

12 passed in 0.35s

[Hook: PreToolUse] Tests passed ✅ — proceeding with commit

If the tests pass, the commit goes through:

[main (root-commit) a1b2c3d] feat: build notes-cli with 3 commands (add, list, search)
 15 files changed, 450 insertions(+)
 create mode 100644 CLAUDE.md
 create mode 100644 .claude/settings.json
 create mode 100644 .claude/skills/add-tests/SKILL.md
 create mode 100644 .claude/skills/create-command/SKILL.md
 create mode 100644 .gitignore
 create mode 100644 pyproject.toml
 create mode 100644 src/__init__.py
 create mode 100644 src/cli.py
 create mode 100644 src/commands/__init__.py
 create mode 100644 src/commands/add.py
 create mode 100644 src/commands/list_notes.py
 create mode 100644 src/commands/search.py
 create mode 100644 src/utils.py
 create mode 100644 tests/__init__.py
 create mode 100644 tests/test_add.py
 create mode 100644 tests/test_list.py
 create mode 100644 tests/test_search.py

If the tests fail before the commit

[Hook: PreToolUse] Detected: git commit
[Hook: PreToolUse] Running: pytest --tb=short -q

FAILED tests/test_list.py::TestList::test_list_json_output
1 failed, 11 passed in 0.33s

[Hook: PreToolUse] Tests failed ❌ — commit blocked

Claude sees that the hook blocked the commit. It should automatically:

  1. Analyze which test failed
  2. Fix the code or the test
  3. Retry the commit
The hook blocked the commit because test_list_json_output failed.
I see the JSON format doesn't include indentation. Fixing it...

[Modifies list_notes.py]
[Hook: PostToolUse] ruff check: All checks passed!

Retrying commit...

[Hook: PreToolUse] Running: pytest --tb=short -q
12 passed in 0.35s
[Hook: PreToolUse] Tests passed ✅

[main a1b2c3d] feat: build notes-cli with 3 commands

This is the power of hooks: the commit can't go through without green tests. It's an automatic safety net.


Step 5: Create a PR (optional)

If your project is on GitHub, you can open a Pull Request:

Create a pull request with a detailed description of what we
built. Include:
- What the CLI is
- The 3 commands with usage examples
- The tech stack
- How to run the tests

Claude runs:

# Create the repository on GitHub (if it doesn't exist)
gh repo create notes-cli --public --source=. --push

# Create the PR
gh pr create --title "feat: notes-cli - CLI tool built with Claude Code" \
  --body "## Summary
  CLI tool for managing notes from the terminal.
  
  ### Commands
  - \`add\` - Create a note with text and optional tag
  - \`list\` - List all notes with tag filter and JSON output
  - \`search\` - Search notes by text (case-insensitive)
  
  ### Tech Stack
  - Python 3.12 + Click 8.1
  - pytest for testing (12 tests, all passing)
  - Ruff for linting
  
  ### How to test
  \`\`\`bash
  pip install -e '.[dev]'
  pytest -v
  python -m src.cli --help
  \`\`\`
  
  ### Built with
  Claude Code workflow: CLAUDE.md → Skills → Hooks → Explore → Plan → Code → Tests"

If you don't use GitHub, you don't need this step. The local commit is enough.


Step 6: Final verification

Run the project's complete verification:

Do a final verification of the whole project. Run:
1. pytest -v (all the tests)
2. ruff check src/ (linting)
3. python -m src.cli --help (working CLI)
4. git log --oneline (commits)
5. Check that CLAUDE.md, skills, and hooks exist

Expected output:

## Final verification

### Tests ✅
12 passed, 0 failed, 0 errors

### Linting ✅
All checks passed!

### CLI ✅
3 commands: add, list, search
--help works for all of them

### Git ✅
1 commit: "feat: build notes-cli with 3 commands"
Working tree clean

### Setup ✅
- CLAUDE.md: 52 lines, 6 sections ✅
- Skills: create-command.md, add-tests.md ✅
- Hooks: PostToolUse (ruff), PreToolUse (pytest) ✅

### Result: Project complete ✅

Final project checklist

This is the definitive checklist. Each item maps to an evaluation criterion:

CLAUDE.md (20%)

  • It exists at the root of the project
  • It has the 6 sections: Description, Stack, Structure, Conventions, Commands, Rules
  • Under 200 lines
  • Specific conventions (not generic ones like "write good code")
  • Runnable commands (copy-paste works)
  • Actionable rules (what NOT to do)
  • It reflects the project's current state (no placeholders left)

Skills (15%)

  • At least 2 skills in .claude/skills/
  • The /create-command skill created and working
  • The /add-tests skill created and working
  • The skills have clear instructions, a template, and rules
  • At least 1 skill was used during the implementation

Hooks (15%)

  • At least 2 hooks in .claude/settings.json
  • The PostToolUse hook (Write → linter) configured
  • The PreToolUse hook (Execute → tests before commit) configured
  • The hooks fired during the implementation
  • The pre-commit hook blocked or allowed commits correctly

Explore → Plan → Code workflow (20%)

  • Explore was used to analyze the setup before implementing
  • Plan was used to design the CLI before coding
  • The plan was iterated at least once with feedback
  • The plan was followed during the implementation
  • The implementation was verified halfway through

Working CLI (20%)

  • The CLI runs with --help
  • 3 or more working commands
  • Every command has help text
  • Every command handles errors correctly
  • The commands produce readable output

Tests (10%)

  • At least 3 tests that pass
  • The tests cover all 3 commands
  • The tests include happy paths and edge cases
  • The tests run with pytest or npm test
  • The tests pass from a clean state (they don't depend on prior state)

Self-assessment rubric

Use this table to evaluate your own work:

CriterionWeightYour score (1-5)Notes
CLAUDE.md20%___
Skills15%___
Hooks15%___
E→P→C workflow20%___
Working CLI20%___
Tests10%___

Scale:

  • 5: Excellent — exceeds expectations
  • 4: Good — meets every requirement
  • 3: Adequate — meets the minimum
  • 2: Needs improvement — something important is missing
  • 1: Insufficient — doesn't meet the criterion

Total score: A weighted sum. Example: if you scored 4 on everything = 4.0 / 5.0 = 80%.


Reflection

Take a moment to answer these questions. There are no right answers — this is so you process what you learned:

On the workflow

  1. How long did the complete project take you? More or less than you expected?
  2. Which phase was the most valuable — Setup, Explore/Plan, Build, or Test?
  3. Would you have gotten the same result without the Explore → Plan → Code cycle?
  4. At what point did Claude Code surprise you in a good way?
  5. At what point did you have to correct Claude?

On the tools

  1. Did CLAUDE.md make a difference in the quality of Claude's output?
  2. Did the skills save time vs writing instructions in every prompt?
  3. Did the hooks catch an error you wouldn't have seen?
  4. Did you use subagents (Explore) during the implementation?

On your process

  1. How does this workflow compare to your usual development process?
  2. What would you change if you did the project again?
  3. Which technique from this module would you adopt in your daily work?

Troubleshooting

"The tests fail with an import error"

# Make sure you install in editable mode
pip install -e ".[dev]"

# Or adjust PYTHONPATH
export PYTHONPATH="${PYTHONPATH}:$(pwd)"

"The pre-commit hook doesn't fire"

Check that settings.json is in .claude/:

cat .claude/settings.json

Check that the hook's condition is right. The matcher must be "Execute" and the command must check that the input contains git commit.

"pytest can't find the tests"

Check that pytest.ini or pyproject.toml has the right configuration:

[tool.pytest.ini_options]
testpaths = ["tests"]

And that the test files start with test_:

ls tests/
# test_add.py  test_list.py  test_search.py

"git commit includes files it shouldn't"

Check .gitignore:

cat .gitignore

Make sure it includes:

  • .notes.json (test data)
  • __pycache__/
  • .claude/settings.local.json
  • node_modules/ (if you're using TypeScript)

If you already committed the wrong files:

git rm --cached .notes.json
echo ".notes.json" >> .gitignore
git commit -m "chore: remove data file from tracking"

"The CLI works but the tests fail"

This usually happens when the tests depend on a .notes.json file left over from earlier sessions. The tests must use tmp_path or isolated_filesystem to create a clean environment:

def test_example(self, tmp_path: Path) -> None:
    with self.runner.isolated_filesystem(temp_dir=tmp_path):
        # No pre-existing .notes.json here
        result = self.runner.invoke(cli, ["add", "Test"])
        assert result.exit_code == 0

"gh pr create fails"

# Check authentication
gh auth status

# If you're not authenticated
gh auth login

# If the repo doesn't exist on GitHub
gh repo create my-cli --public --source=. --push

Comparison: manual testing vs testing with Claude Code

Manual testing (without Claude Code)

1. You think about which tests to write
2. You write each test by hand
3. You run them and debug
4. You repeat for every command
5. You forget edge cases
6. Total: 30-45 minutes

Testing with Claude Code + a skill

1. You invoke /add-tests src/commands/add.py
2. Claude analyzes the command and generates the tests
3. Claude runs the tests
4. If they fail, Claude fixes them
5. You repeat for every command
6. Total: 5-10 minutes

What Claude does better than you in testing

AspectYouClaude Code
Spotting edge casesYou think of the obvious onesIt analyzes the code and finds more
BoilerplateYou write it every timeIt generates it from the skill
ConsistencyIt varies between testsIt always follows the skill's template
CoverageSometimes you forget casesIt generates at least 3 tests per function

What you do better than Claude in testing

AspectClaude CodeYou
Prioritizing what to testEverything is equalYou know what's critical
Complex integration testsIt sometimes gets confusedYou understand the business flow
Defining what "correct" meansIt assumesYou define the criteria
UX testsIt can'tYou can judge whether the output looks right

The ideal combination: Claude generates, you review and prioritize.


Project complete

If you got here with every item on the checklist ticked: you finished the capstone project.

What you achieved

  1. You configured a professional environment — CLAUDE.md, skills, hooks. You're not a casual user.
  2. You followed the E→P→C workflow — you analyzed, planned, and executed, in order. You didn't improvise.
  3. You built something real — a working CLI with 3 commands that you can use and show off.
  4. You automated validations — the hooks ran the linter and the tests without you lifting a finger.
  5. You generated tests — Claude Code didn't just write code, it validated it too.
  6. You versioned properly — a commit with a descriptive message, tests passing, clean code.

What that proves

You didn't prove you know how to program a CLI (anyone with a tutorial can do that). You proved you know how to work with an AI agent professionally:

  • You configure the environment before implementing
  • You analyze before acting
  • You plan before coding
  • You automate what can be automated
  • You validate before delivering

That's the competency that sets you apart.


What's next

You finished the Claude Code Foundations guide. You now have the foundation to work with Claude Code professionally.

The next guide in the Claude Code Agentic Development path is:

Prompt Engineering with Claude Code

In that guide you'll go deep on how to communicate with Claude Code optimally:

  • Prompts as behavior contracts — how to write instructions that produce predictable results
  • Zero-shot and few-shot prompting — when to give examples and when not to
  • Chain-of-thought — how to ask Claude to reason step by step
  • Prompt evaluation — how to measure whether your prompts are effective
  • Prompts in real systems — CLAUDE.md, skills, and hooks optimized with prompt engineering

The difference between this guide and the next one: here you learned the tools (CLAUDE.md, skills, hooks, workflow). There you'll learn the language (how to talk to Claude Code to get the best results).


Summary of the full module

CapsulePhaseWhat you didTime
01IntroUnderstood the project, the criteria, and the timeline10 min
02SetupCLAUDE.md + 2 skills + 2 hooks + entry point25 min
03Explore + PlanAnalyze the setup, design the CLI, iterate the plan15 min
04BuildImplement 3 commands with Claude Code35 min
05Test + CommitTests, review, commit, delivery15 min
TotalComplete project~1.5 hrs

The workflow you now command

CLAUDE.md → Skills → Hooks → Explore → Plan → Code → Tests → Commit
    │          │        │         │        │       │       │        │
    ▼          ▼        ▼         ▼        ▼       ▼       ▼        ▼
 Context    Reusable  Ongoing  Analyze  Design  Build   Validate Version
 persists   shortcuts automa-  before   before  step by before   with
                      tion     acting   coding  step    deliver- confidence
                                                        ing

This isn't just a workflow for this project. It's a workflow for any project where you work with Claude Code — or with any AI agent.

Guide complete. Good work.