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.ts with storage and formatting functions
  • 3 command files in src/commands/
  • src/cli.py / src/index.ts updated with the 3 commands registered
  • A CLI that runs with --help and 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:

  1. Ask for the implementation step by step (not all at once)
  2. Review what Claude produces
  3. Give feedback when something doesn't fit
  4. Use skills where they apply
  5. 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:

  1. Claude reads CLAUDE.md — it uses the conventions defined there
  2. Claude creates the file — it writes src/utils.py with the functions
  3. 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

  1. Claude reads .claude/skills/create-command/SKILL.md
  2. It pulls the skill's instructions in as context
  3. It creates src/commands/list_notes.py following the skill's template
  4. It registers the command in src/cli.py
  5. 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__.py in the commands/ 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:

SituationExample 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:

SituationExample
Implementation detailsHow to parse the JSON internally
Naming of local variablesVariable names inside functions
Minor imports and dependenciesWhich pathlib function to use
The order of internal operationsWhat 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

StrategyWhen to use it
Implement step by stepAlways — don't ask for everything at once
Use /compactWhen the conversation hits 15+ messages
New sessionIf 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.py is implemented with storage and formatting functions
  • src/commands/add.py is implemented and registered
  • src/commands/list_notes.py is implemented and registered
  • src/commands/search.py is implemented and registered
  • The CLI runs with --help showing the 3 commands
  • The add command creates notes correctly
  • The list command shows notes with a filter and JSON
  • The search command searches case-insensitively
  • The linter hooks fired during the implementation
  • The /create-command skill was used at least once

Summary

What you did in this capsule:

  1. Implemented utils.py — the storage and formatting foundation every command uses
  2. Implemented 3 commands — add, list, search — each in its own file
  3. Used the /create-command skill to create at least one command with consistency
  4. Watched hooks in action — the linter ran automatically after every file
  5. Used the Explore subagent to verify the implementation halfway through
  6. 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:

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

  2. Skills make the second command easier than the first. The /create-command skill's template guarantees consistency without you having to remember every detail. The time you invested in creating skills pays off immediately.

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