Module 8: Capstone project: Your first project with Claude Code
Building with Claude Code: Implementation
Building with Claude Code: Implementation
Project goal
Build a working CLI tool using the complete Claude Code workflow. In this capsule you complete Phase 3: Build — you'll implement your CLI's 3 commands using Agent mode, skills, hooks, and subagents.
What you built in previous modules
- Capsule 02: The complete setup (CLAUDE.md, skills, hooks, entry point)
- Capsule 03: Explore of the project + an approved implementation plan
- Module 04: The Explore → Plan → Code workflow
- Module 05: Skills and hooks in action
- Module 06: Subagents for delegation
What you'll add in this module
By the end of this capsule you'll have:
src/utils.py/src/utils.tswith storage and formatting functions- 3 command files in
src/commands/ src/cli.py/src/index.tsupdated with the 3 commands registered- A CLI that runs with
--helpand 3 working commands - Hooks firing automatically during the implementation
Technical specifications
What Claude Code is going to do
In this phase, Claude Code operates in Agent mode (the default). It has full permission to:
- Create and modify files
- Run terminal commands
- Install dependencies
- Read the project's files
Your role
You direct. Claude executes. Your job is to:
- Ask for the implementation step by step (not all at once)
- Review what Claude produces
- Give feedback when something doesn't fit
- Use skills where they apply
- Watch the hooks in action
The fundamental rule
Implement one command at a time. Don't ask for all 3 commands at once. Each command is a cycle:
Ask → Claude implements → Verify → Next
Step-by-step walkthrough
Step 1: Start the implementation session
If you closed the previous session, open a new one:
cd my-cli-project
claude
If you stayed in the same session, Claude already has the plan's context. If it's a new session, remind it:
We're going to implement the CLI we planned. The plan is:
1. src/utils.py — Storage and formatting
2. src/commands/add.py — add command
3. src/commands/list_notes.py — list command
4. src/commands/search.py — search command
5. src/cli.py — Register the commands
Let's start with utils.py. Implement it according to the plan.
Step 2: Implement utils (the foundation)
Prompt
Implement src/utils.py with the storage and formatting functions
we planned:
- load_notes(): load notes from the JSON file
- save_notes(): save notes to the JSON file
- get_notes_path(): return the path to the storage file
- format_note(): format a note for display
Follow CLAUDE.md's conventions.
What to watch during the implementation
While Claude works, watch for:
- Claude reads CLAUDE.md — it uses the conventions defined there
- Claude creates the file — it writes
src/utils.pywith the functions - The PostToolUse hook fires — after the file is created, the linter (ruff/eslint) runs automatically
The hook in action looks like this in the terminal:
Claude wrote src/utils.py
[Hook: PostToolUse] Running: ruff check --fix src/utils.py
All checks passed!
If the linter finds style errors:
[Hook: PostToolUse] Running: ruff check --fix src/utils.py
Found 2 errors (2 fixed)
Claude sees this output and knows the hook fixed the errors. You don't have to do anything.
Verify utils
Once Claude has created the file, check it:
Show me the contents of src/utils.py and confirm the
functions have type hints and docstrings.
Claude shows you the code. Check that:
- The functions have the right signatures
- There are type hints on parameters and return types
- Storage uses the path defined in the plan
- Error handling is appropriate (file doesn't exist, invalid JSON)
If you don't like something:
The format_note function doesn't include the tag in the output.
Fix it — the format should be: #42 Text [tag] timestamp
Claude modifies the file. The linter hook runs again automatically.
Step 3: Implement the first command
Prompt
Implement the first command: add. Create src/commands/add.py
following the plan. The command should:
- Take the note's text as an argument
- Accept --tag to categorize it
- Save the note using utils.save_notes
- Print a confirmation with the created note's ID
Then register it in src/cli.py.
The hook in action
When Claude creates src/commands/add.py, the PostToolUse hook runs:
Claude wrote src/commands/add.py
[Hook: PostToolUse] Running: ruff check --fix src/commands/add.py
All checks passed!
When Claude modifies src/cli.py to register the command:
Claude wrote src/cli.py
[Hook: PostToolUse] Running: ruff check --fix src/cli.py
All checks passed!
Verify the first command
Ask Claude to run the command:
Run the add command to check that it works:
python -m src.cli add "My first note" --tag test
Claude runs it and you should see something like:
$ python -m src.cli add "My first note" --tag test
Note created: #1
Check that the storage file was created:
Was the .notes.json file created? Show me its contents.
Claude reads the file and shows you:
{
"notes": [
{
"id": 1,
"text": "My first note",
"tag": "test",
"created_at": "2026-02-28T10:30:00"
}
],
"next_id": 2
}
If everything looks right, move on to the second command.
Step 4: Implement the second command using the skill
This is where the /create-command skill proves its worth.
Using the skill
/create-command list_notes
The list command should:
- List every note
- Filter by tag with --tag
- Support JSON output with --json
- Show formatted output by default
What happens when you use the skill
- Claude reads
.claude/skills/create-command/SKILL.md - It pulls the skill's instructions in as context
- It creates
src/commands/list_notes.pyfollowing the skill's template - It registers the command in
src/cli.py - The linter hook runs automatically
The skill guarantees consistency: the file's format, the command's structure, the error handling, and the registration in cli.py all follow the same pattern as the first command.
Verify with the skill
If the skill includes a checklist (like the one you created in Capsule 02), Claude follows it:
Check the skill's checklist:
- [ ] File created in src/commands/
- [ ] Type hints on parameters
- [ ] Docstring as help text
- [ ] Errors with ClickException
- [ ] Registered in cli.py
Test the second command
Run these commands to check list:
python -m src.cli add "Second note" --tag work
python -m src.cli add "Third note" --tag personal
python -m src.cli list
python -m src.cli list --tag work
python -m src.cli list --json
You should see:
$ python -m src.cli list
#1 My first note [test] Feb 28, 10:30
#2 Second note [work] Feb 28, 10:32
#3 Third note [personal] Feb 28, 10:33
$ python -m src.cli list --tag work
#2 Second note [work] Feb 28, 10:32
$ python -m src.cli list --json
[
{"id": 1, "text": "My first note", "tag": "test", ...},
{"id": 2, "text": "Second note", "tag": "work", ...},
{"id": 3, "text": "Third note", "tag": "personal", ...}
]
Step 5: Verify the implementation with a subagent
Before implementing the third command, check that everything built so far is coherent. Claude Code can use its Explore subagent for this:
Review everything we've implemented so far.
Check that:
1. utils.py is used correctly by add.py and list_notes.py
2. CLAUDE.md's conventions are respected
3. There are no unused imports or dead functions
4. Error handling is consistent across commands
Just analyze, don't change anything yet.
Claude uses its analysis capability to review the files and give you a report:
## Implementation review
### Consistency ✅
- Both commands use utils.load_notes() and utils.save_notes()
- Naming follows snake_case as CLAUDE.md defines
- Error handling with click.ClickException in both
### Issues found
1. ⚠️ list_notes.py imports json but doesn't use it when --json
isn't active. Not an error, but ruff will flag it as an
unused import if --fix doesn't resolve it.
→ Move the import inside the if --json branch
2. ✅ add.py: no issues
3. ✅ utils.py: no issues
4. ✅ cli.py: both commands registered correctly
### Recommendation
Fix the json import in list_notes.py before continuing.
If there are issues, ask it to fix them:
Fix the json import in list_notes.py as you suggest.
Step 6: Implement the third command
Implement the third command: search. Create src/commands/search.py.
The command should:
- Take a query as an argument
- Search the text of every note (case-insensitive)
- Show matching notes in the same format as list
- Show "No notes found" if there are no results
Claude creates the file, the hook runs the linter, and the command is registered in cli.py.
Test the third command
Test search with these cases:
python -m src.cli search "note"
python -m src.cli search "FIRST"
python -m src.cli search "nonexistent"
Expected results:
$ python -m src.cli search "note"
#1 My first note [test] Feb 28, 10:30
#2 Second note [work] Feb 28, 10:32
#3 Third note [personal] Feb 28, 10:33
$ python -m src.cli search "FIRST"
#1 My first note [test] Feb 28, 10:30
$ python -m src.cli search "nonexistent"
No notes found matching "nonexistent"
Step 7: Run the complete CLI
Now that all 3 commands are implemented, check the CLI as a whole:
Run the CLI with --help to see every available command.
$ python -m src.cli --help
Usage: cli [OPTIONS] COMMAND [ARGS]...
notes-cli - Notes tool for the terminal.
Options:
--version Show the version and exit.
--help Show this message and exit.
Commands:
add Add a new note.
list List all notes.
search Search notes by text.
Check the help for each command:
Run --help for each command: add, list, and search.
$ python -m src.cli add --help
Usage: cli add [OPTIONS] TEXT
Add a new note.
Options:
--tag TEXT Tag to categorize the note
--help Show this message and exit.
$ python -m src.cli list --help
Usage: cli list [OPTIONS]
List all notes.
Options:
--tag TEXT Filter by tag
--json / -j Output in JSON format
--help Show this message and exit.
$ python -m src.cli search --help
Usage: cli search [OPTIONS] QUERY
Search notes by text.
Options:
--help Show this message and exit.
Handling errors during the implementation
Error: Claude generates code that doesn't work
If a command fails when it runs:
The search command fails with "ModuleNotFoundError: No module
named 'src.commands.search'". What happened?
Claude diagnoses and fixes it. Common causes:
- A missing
__init__.pyin thecommands/directory - An incorrect import path
- The filename doesn't match the import
Error: The linter hook fails
If the hook shows errors that don't get fixed automatically:
[Hook: PostToolUse] Running: ruff check --fix src/commands/search.py
src/commands/search.py:15:5: F811 Redefinition of unused `result`
1 error remaining (not auto-fixable)
Claude sees this output and should fix the error automatically. If it doesn't:
The ruff hook found an F811 error in search.py.
Fix it.
Error: The output doesn't look the way you expected
The list output isn't aligned. The columns don't line up.
Adjust format_note so it uses a fixed column width.
Claude adjusts the formatting function.
Error: Storage doesn't persist
The notes disappear between runs. Is the .notes.json file
being created correctly?
Claude investigates and fixes the path or the save logic.
When to steer vs when to let Claude decide
Steer Claude when:
| Situation | Example of steering |
|---|---|
| The decision affects the UX | "I want the output to have colors" |
| The plan is specific | "Use the #42 format like we planned" |
| There's a personal preference | "I prefer exceptions over return codes" |
| The behavior is ambiguous | "If there are no notes, show a message, not an empty list" |
Let Claude decide when:
| Situation | Example |
|---|---|
| Implementation details | How to parse the JSON internally |
| Naming of local variables | Variable names inside functions |
| Minor imports and dependencies | Which pathlib function to use |
| The order of internal operations | What order to run the validations in |
The golden rule
Steer the "what" and the "how it looks". Let Claude solve the "how it works internally".
Tips for effective prompts during the implementation
Be incremental
❌ "Implement the whole CLI with the 3 commands, utils, tests,
and register everything in cli.py"
✅ "Implement src/utils.py with the storage functions"
Give context when you switch steps
✅ "Utils is done and works. Now implement the add
command following the plan."
Include verification criteria
✅ "Implement search. When you're done, run it with
'python -m src.cli search note' to verify."
Reference the plan
✅ "According to the plan, search must be case-insensitive. Make sure
you implement that."
Ask it to run things to verify
✅ "Run the CLI with --help to check that the 3 commands
show up."
The end state after this capsule
Your project should look like this:
my-cli-project/
├── CLAUDE.md
├── .claude/
│ ├── skills/
│ │ ├── create-command.md
│ │ └── add-tests.md
│ └── settings.json
├── src/
│ ├── __init__.py
│ ├── cli.py ← Updated with 3 commands
│ ├── commands/
│ │ ├── __init__.py
│ │ ├── add.py ← add command
│ │ ├── list_notes.py ← list command
│ │ └── search.py ← search command
│ └── utils.py ← Storage and formatting
├── tests/
│ └── __init__.py ← (empty, tests in capsule 05)
├── .notes.json ← Storage file (test data)
├── pyproject.toml
└── .gitignore
Final verification
Run this full sequence to check that everything works:
# Clean up the test data
rm -f .notes.json
# Check help
python -m src.cli --help
# Add notes
python -m src.cli add "Buy coffee" --tag personal
python -m src.cli add "Review PR #42" --tag work
python -m src.cli add "Study Claude Code" --tag learning
# List all
python -m src.cli list
# Filter by tag
python -m src.cli list --tag work
# JSON output
python -m src.cli list --json
# Search
python -m src.cli search "Claude"
python -m src.cli search "COFFEE"
python -m src.cli search "nonexistent"
If every command works correctly, Phase 3 is complete.
Specific troubleshooting
"ModuleNotFoundError"
# Check that you're in the right directory
pwd
# Check that the package is installed
pip install -e .
# Check that __init__.py exists in every directory
find src -name "__init__.py"
"Click doesn't recognize the command"
Check that the command is registered in cli.py:
from src.commands.add import add
from src.commands.list_notes import list_notes
from src.commands.search import search
cli.add_command(add)
cli.add_command(list_notes, "list")
cli.add_command(search)
Note the "list" as the second argument — it renames the command from list_notes to list in the CLI.
"JSON decode error"
The .notes.json file may be corrupted. Delete it and start over:
rm .notes.json
python -m src.cli add "Test"
"Permission denied when creating the file"
Check the directory's permissions:
ls -la .
"Commander/Click doesn't recognize the options"
Check that the decorators or option-definition methods are correct:
Python — Click:
@click.option("--tag", default=None, help="Tag to filter by")
TypeScript — Commander:
.option("--tag <tag>", "Tag to filter by")
The most common mistake is forgetting the argument's type in Commander (<tag> for required, [tag] for optional).
"The hook doesn't run ruff/eslint"
Check that the linter is installed:
# Python
ruff --version
# TypeScript
npx eslint --version
If it isn't installed:
# Python
pip install ruff
# TypeScript
npm install --save-dev eslint
The context window during the implementation
How context gets consumed
Every interaction with Claude Code consumes tokens from the context window. During the implementation, the context fills up with:
- CLAUDE.md (~500 tokens, always present)
- Files Claude reads (each file ~200-500 tokens)
- The output of the commands it runs
- Your conversation (every message)
- Loaded skills (when you invoke them)
Signs that the context is filling up
- Claude starts "forgetting" earlier decisions
- The answers get more generic
- Claude repeats questions you already answered
Strategies for managing context
| Strategy | When to use it |
|---|---|
| Implement step by step | Always — don't ask for everything at once |
Use /compact | When the conversation hits 15+ messages |
| New session | If Claude starts "forgetting" the plan |
| Point to CLAUDE.md | "Follow CLAUDE.md's conventions" instead of repeating them |
| Point to the plan | "According to the plan..." instead of re-explaining the architecture |
If you need a new session mid-implementation
CLAUDE.md persists between sessions — you don't need to reconfigure it. Just start the new session and remind it where you were:
We're implementing a notes CLI. I already finished utils.py
and the add command. I need to implement list_notes and search
following the project's conventions. Read the existing code
and continue.
Claude reads the existing files and picks up where you left off.
Completion checklist
Before moving on to Capsule 05, check that:
-
src/utils.pyis implemented with storage and formatting functions -
src/commands/add.pyis implemented and registered -
src/commands/list_notes.pyis implemented and registered -
src/commands/search.pyis implemented and registered - The CLI runs with
--helpshowing the 3 commands - The
addcommand creates notes correctly - The
listcommand shows notes with a filter and JSON - The
searchcommand searches case-insensitively - The linter hooks fired during the implementation
- The
/create-commandskill was used at least once
Summary
What you did in this capsule:
- Implemented utils.py — the storage and formatting foundation every command uses
- Implemented 3 commands — add, list, search — each in its own file
- Used the
/create-commandskill to create at least one command with consistency - Watched hooks in action — the linter ran automatically after every file
- Used the Explore subagent to verify the implementation halfway through
- Ran the complete CLI and verified all 3 commands work
The implementation followed the plan you designed in the previous capsule. You didn't improvise the architecture — you executed it.
Lessons from the Build phase
Three things you should have noticed during the implementation:
-
Claude Code is more effective with incremental instructions. "Implement utils.py" produces better code than "implement the whole CLI". That's the multi-turn pattern you learned in Module 04.
-
Skills make the second command easier than the first. The
/create-commandskill's template guarantees consistency without you having to remember every detail. The time you invested in creating skills pays off immediately. -
Hooks are a silent safety net. You don't invoke them, you don't think about them — they just work. Every file Claude writes goes through the linter automatically. It's code quality with no effort.
These three lessons apply to any project, not just CLIs. When you work on your next project with Claude Code, apply the same pattern: incremental, with skills, with hooks.
Next capsule: 05 - Tests, commit, and deliver — where you'll generate tests, review the code, commit, and finish the project.