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:
- Analyze which test failed
- Fix the code or the test
- 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-commandskill created and working - The
/add-testsskill 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
pytestornpm 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:
| Criterion | Weight | Your score (1-5) | Notes |
|---|---|---|---|
| CLAUDE.md | 20% | ___ | |
| Skills | 15% | ___ | |
| Hooks | 15% | ___ | |
| E→P→C workflow | 20% | ___ | |
| Working CLI | 20% | ___ | |
| Tests | 10% | ___ |
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
- How long did the complete project take you? More or less than you expected?
- Which phase was the most valuable — Setup, Explore/Plan, Build, or Test?
- Would you have gotten the same result without the Explore → Plan → Code cycle?
- At what point did Claude Code surprise you in a good way?
- At what point did you have to correct Claude?
On the tools
- Did CLAUDE.md make a difference in the quality of Claude's output?
- Did the skills save time vs writing instructions in every prompt?
- Did the hooks catch an error you wouldn't have seen?
- Did you use subagents (Explore) during the implementation?
On your process
- How does this workflow compare to your usual development process?
- What would you change if you did the project again?
- 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.jsonnode_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
| Aspect | You | Claude Code |
|---|---|---|
| Spotting edge cases | You think of the obvious ones | It analyzes the code and finds more |
| Boilerplate | You write it every time | It generates it from the skill |
| Consistency | It varies between tests | It always follows the skill's template |
| Coverage | Sometimes you forget cases | It generates at least 3 tests per function |
What you do better than Claude in testing
| Aspect | Claude Code | You |
|---|---|---|
| Prioritizing what to test | Everything is equal | You know what's critical |
| Complex integration tests | It sometimes gets confused | You understand the business flow |
| Defining what "correct" means | It assumes | You define the criteria |
| UX tests | It can't | You 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
- You configured a professional environment — CLAUDE.md, skills, hooks. You're not a casual user.
- You followed the E→P→C workflow — you analyzed, planned, and executed, in order. You didn't improvise.
- You built something real — a working CLI with 3 commands that you can use and show off.
- You automated validations — the hooks ran the linter and the tests without you lifting a finger.
- You generated tests — Claude Code didn't just write code, it validated it too.
- 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
| Capsule | Phase | What you did | Time |
|---|---|---|---|
| 01 | Intro | Understood the project, the criteria, and the timeline | 10 min |
| 02 | Setup | CLAUDE.md + 2 skills + 2 hooks + entry point | 25 min |
| 03 | Explore + Plan | Analyze the setup, design the CLI, iterate the plan | 15 min |
| 04 | Build | Implement 3 commands with Claude Code | 35 min |
| 05 | Test + Commit | Tests, review, commit, delivery | 15 min |
| Total | Complete 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.