Build a Python Task Tracker CLI
Use Claude Code's agentic workflows to build a Python CLI task tracker.
Introduction
30 Second Summary
AI coding tools can now write entire features from a single prompt. The real skill is learning how to steer them effectively so you get production-quality code instead of a mess you have to rewrite.
In this project, you will build a Python command-line task tracker using Claude Code as your primary development tool. The app itself is simple. The real deliverable is your muscle memory for Claude Code's core workflows.
What You'll Build
A Python CLI task tracker that lets you add, list, complete, and delete tasks with priorities and filtering, built almost entirely through Claude Code's agentic workflows.
By the end of this project, you'll have:
- A configured Claude Code environment with a custom CLAUDE.md that encodes your project's conventions.
- A working Python CLI app using the Click library with task CRUD operations.
- A test suite written by Claude Code using pytest, plus a Git repository with commits created through Claude Code.
- Secret Mission: Add a stats command that shows task completion metrics and streaks.
Are there any prerequisites?
This project assumes familiarity with Python, terminal basics, and Git. You also need a Claude Pro subscription (approximately $20/month) and WSL2 with Ubuntu 24.04 already installed on your machine.
Before We Start
You are about to build a Python CLI task tracker using Claude Code as your primary development tool. The goal is not the app itself. You could write a task tracker by hand in 20 minutes.
The real goal is to internalize Claude Code's agentic workflows so you can apply them to real projects. By the end, you will know how to configure Claude Code for a Python project, generate features through natural language, use plan mode for complex changes, and have Claude Code debug issues and write tests for you.
In this step, get ready to:
- Install Claude Code and authenticate with your Claude Pro account.
- Create and initialize the project with Git, a virtual environment, and dependencies.
- Write a CLAUDE.md that teaches Claude your project's conventions.
Install Claude Code on WSL2
Claude Code is a terminal-based AI coding assistant that lives in your command line. It reads your codebase, proposes edits, runs commands, and iterates until your feature works.
- Open your Ubuntu terminal in WSL2.
- Install Claude Code by running this command:
curl -fsSL https://claude.ai/install.sh | bash
What does this command do?
This downloads and runs the official Claude Code installer script. It installs the claude binary on your system. Native installations automatically update in the background, so you never need to manage versions manually.
- After installation completes, verify it worked by running:
claude --version
You should see a version number printed in the terminal.
- Start Claude Code to trigger the login flow by running:
claude
What happens when you run this?
Claude Code opens a browser-based authentication prompt. Follow the steps to log in with your Claude Pro account. Once authenticated, you will see an interactive session in your terminal.
- Once you see the interactive session is active, exit by typing /exit or pressing Ctrl+D.
Authentication not working?
Make sure you are logging in with a Claude Pro, Max, Team, or Enterprise account. Claude Code requires an active subscription.
If the browser does not open automatically, look for a URL in your terminal output and open it manually.
Help me troubleshoot Claude Code authentication on WSL2.
Create the project directory and initialize it
Every Python project needs a dedicated directory, a Git repository for version control, and a virtual environment to isolate its dependencies.
- Create your project directory, initialize Git, set up a virtual environment, and install dependencies by running these commands:
mkdir ~/task-tracker && cd ~/task-tracker
git init
python3 -m venv venv
source venv/bin/activate
pip install click==8.4.1 pytest==9.1.0
pip freeze > requirements.txt
What do these commands do?
- mkdir ~/task-tracker && cd ~/task-tracker creates the project folder in your home directory and moves into it.
- git init initializes a Git repository so Claude Code can make commits for you later.
- python3 -m venv venv creates an isolated Python environment in a folder called venv.
- source venv/bin/activate activates that environment so packages install here instead of globally.
- pip install click==8.4.1 pytest==9.1.0 installs the two libraries the project depends on at their pinned versions.
- pip freeze > requirements.txt writes the installed packages to a file so anyone can reproduce the environment.
- Confirm everything is set up correctly by running:
ls
You should see venv and requirements.txt in the output. Your terminal prompt should also show (venv) at the beginning, confirming the virtual environment is active.
Don't see (venv) in your prompt?
If you copy-pasted the commands as a block, the cd ~/task-tracker may not have taken effect in the same shell session. Run cd ~/task-tracker again, then source venv/bin/activate.
Help me activate my Python virtual environment on WSL2.
Create a CLAUDE.md file
Claude Code loads a CLAUDE.md file at the start of every session. This file teaches Claude your project's conventions, structure, and commands without you having to repeat yourself each time.
- Create a new file called CLAUDE.md in the ~/task-tracker directory. You can use nano by running:
nano CLAUDE.md
- Paste the following content into the file:
# Task Tracker CLI
## Project overview
A Python CLI task tracker using Click. Tasks are stored as JSON in a local file.
## Tech stack
- Python 3 with type hints
- Click for CLI interface
- pytest for testing
- JSON file storage (tasks.json)
## Conventions
- Use Google-style docstrings
- Functions should have type annotations
- Keep modules small and focused (one file per concern)
- Use pathlib for file paths, not os.path
## Project structure
- task_tracker/cli.py - Click commands (entry point)
- task_tracker/storage.py - JSON file read/write
- task_tracker/models.py - Task data structures
- tests/ - pytest test files
## Commands
- Run app: python -m task_tracker
- Run tests: pytest
- Run single test file: pytest tests/test_storage.py -v
## Important
- Always store tasks with a UUID as the ID
- Task priorities are: low, medium, high
- Default priority is medium
- Date format is ISO 8601 (YYYY-MM-DD)
- Save the file by pressing Ctrl+O, then Enter to confirm, then Ctrl+X to exit nano.
Why is CLAUDE.md so important?
This file is the single most important productivity unlock in Claude Code. All discovered CLAUDE.md files are concatenated into context at session start. This means Claude already knows your tech stack, file layout, naming conventions, and how to run tests before you ask a single question.
The better your CLAUDE.md, the fewer corrections you need to make. Keep it under 200 lines and focused on decisions that Claude would otherwise guess wrong.
- Verify Claude Code reads your CLAUDE.md by starting a session inside the project:
claude
- Once the session starts, type this prompt:
what does this project do?
Claude should reference your CLAUDE.md contents in its response. It should mention the Click CLI, JSON storage, and task management. This proves Claude has read and understood your project configuration.
- Exit the session with /exit or Ctrl+D.
Claude doesn't mention your project details?
Make sure CLAUDE.md is in the root of ~/task-tracker/ (not inside a subfolder). Run ls ~/task-tracker/CLAUDE.md to confirm the file exists at the right path.
Also confirm you started Claude Code while inside the ~/task-tracker directory. Claude loads CLAUDE.md from the current working directory.
Help me get Claude Code to read my CLAUDE.md file.
✔️ Awesome, I've got everything!
Great. Double check you have saved both CLAUDE.md and requirements.txt in your ~/task-tracker directory.
ⓧ I'd like to double check the full code
Here is what your CLAUDE.md should contain:
# Task Tracker CLI
## Project overview
A Python CLI task tracker using Click. Tasks are stored as JSON in a local file.
## Tech stack
- Python 3 with type hints
- Click for CLI interface
- pytest for testing
- JSON file storage (tasks.json)
## Conventions
- Use Google-style docstrings
- Functions should have type annotations
- Keep modules small and focused (one file per concern)
- Use pathlib for file paths, not os.path
## Project structure
- task_tracker/cli.py - Click commands (entry point)
- task_tracker/storage.py - JSON file read/write
- task_tracker/models.py - Task data structures
- tests/ - pytest test files
## Commands
- Run app: python -m task_tracker
- Run tests: pytest
- Run single test file: pytest tests/test_storage.py -v
## Important
- Always store tasks with a UUID as the ID
- Task priorities are: low, medium, high
- Default priority is medium
- Date format is ISO 8601 (YYYY-MM-DD)
And here is what your requirements.txt should contain:
click==8.4.1
pytest==9.1.0
Your environment is ready. Claude Code knows your project's conventions, structure, and commands. Next up, you will put Claude Code to work generating the entire task tracker through natural language prompts.
Install Claude Code and Set Up Your Project
Before you can delegate coding tasks to Claude Code, you need a working development environment. Claude Code is a terminal-based AI assistant that reads your project files, understands context, and generates code based on natural language instructions.
In this step, you will install the Claude Code CLI on your WSL2 Ubuntu terminal, set up a Python project with version control and dependencies, and create a CLAUDE.md configuration file that teaches Claude how your project works.
In this step, get ready to:
- Install and authenticate Claude Code on WSL2.
- Create a Python project with Git, a virtual environment, and dependencies.
- Write a CLAUDE.md file that configures Claude Code for your project.
Install Claude Code on WSL2
Claude Code runs directly in your terminal and connects to your Claude Pro subscription. You install it with a single command on Ubuntu/WSL2.
- Install Claude Code by running this command in your Ubuntu terminal:
curl -fsSL https://claude.ai/install.sh | bash
What does this command do?
The curl command downloads the installer script from Anthropic's servers. Piping it to bash runs the script immediately, which installs the Claude Code binary on your system.
Native installations automatically update in the background, so you will always have the latest version.
- Verify the installation succeeded by running:
claude --version
- You should see a version number printed in your terminal (something like 2.x.x).
Command not found?
If your terminal says claude is not found, close and reopen your terminal so it picks up the new path. Then try claude --version again.
help me troubleshoot Claude Code installation on WSL2
- Start Claude Code to trigger the authentication flow by running:
claude
- A browser window will open asking you to log in. Sign in with your Claude Pro account.
- Once authenticated, you will see Claude Code's interactive prompt in your terminal.
- Exit the session by typing /exit or pressing Ctrl+D.
What is Claude Code?
Claude Code is an agentic coding tool that lives in your terminal. You describe what you want in plain English, and Claude reads your files, plans changes, and writes code for you.
It uses your existing Claude Pro subscription. There is no separate billing for Claude Code usage.
Create the project directory and initialize it
Every Python project needs a few things: a folder to live in, version control to track changes, a virtual environment to isolate dependencies, and the dependencies themselves.
- Create your project directory, initialize Git, set up a virtual environment, and install your dependencies by running these commands:
mkdir ~/task-tracker && cd ~/task-tracker
git init
python3 -m venv venv
source venv/bin/activate
pip install click==8.4.1 pytest==9.1.0
pip freeze > requirements.txt
What do these commands do?
- mkdir ~/task-tracker && cd ~/task-tracker creates your project folder in your home directory and moves into it.
- git init turns the folder into a Git repository so you can track changes.
- python3 -m venv venv creates an isolated Python environment so your project's packages don't conflict with system packages.
- source venv/bin/activate activates the virtual environment for your current terminal session.
- pip install click==8.4.1 pytest==9.1.0 installs the two libraries the project needs: Click for building CLI commands, and pytest for testing.
- pip freeze > requirements.txt saves the exact versions to a file so anyone can recreate your environment.
- After running these commands, confirm your virtual environment is active. You should see (venv) at the start of your terminal prompt.
Don't see (venv) in your prompt?
If (venv) does not appear, the activation did not stick. Run source venv/bin/activate again from inside the ~/task-tracker folder.
help me activate my Python virtual environment on WSL2
Create a CLAUDE.md file
CLAUDE.md is a configuration file that Claude Code reads at the start of every session. It teaches Claude your project's structure, conventions, and commands so you do not have to repeat yourself.
- Create a file called CLAUDE.md in your ~/task-tracker folder by running:
touch CLAUDE.md
- Open CLAUDE.md in your preferred text editor and paste the following content:
# Task Tracker CLI
## Project overview
A Python CLI task tracker using Click. Tasks are stored as JSON in a local file.
## Tech stack
- Python 3 with type hints
- Click for CLI interface
- pytest for testing
- JSON file storage (tasks.json)
## Conventions
- Use Google-style docstrings
- Functions should have type annotations
- Keep modules small and focused (one file per concern)
- Use pathlib for file paths, not os.path
## Project structure
- task_tracker/cli.py - Click commands (entry point)
- task_tracker/storage.py - JSON file read/write
- task_tracker/models.py - Task data structures
- tests/ - pytest test files
## Commands
- Run app: python -m task_tracker
- Run tests: pytest
- Run single test file: pytest tests/test_storage.py -v
## Important
- Always store tasks with a UUID as the ID
- Task priorities are: low, medium, high
- Default priority is medium
- Date format is ISO 8601 (YYYY-MM-DD)
Why is CLAUDE.md so important?
This file is the single most important productivity unlock in Claude Code. It gets loaded at the start of every session and encodes your project's conventions without you having to repeat yourself.
Notice how it specifies everything from coding style (Google-style docstrings, type hints) to project structure (one file per concern). Claude will follow these rules in every piece of code it generates.
- Save CLAUDE.md.
- Verify Claude Code reads it by starting a session inside your project:
claude
- Once Claude's prompt appears, type this question:
what does this project do?
- Claude should reference your CLAUDE.md contents in its response. It will mention Click, JSON storage, task priorities, or other details from your configuration file.
- Exit the session with /exit or Ctrl+D.
Claude didn't mention your project details?
Make sure you saved CLAUDE.md in the ~/task-tracker folder (not a subfolder). Claude Code looks for it at the project root.
Also confirm you started claude while inside ~/task-tracker. Run pwd to check your current directory.
help me get Claude Code to read my CLAUDE.md file
✔️ Awesome, I've got everything!
Great. Double check you have saved CLAUDE.md and that Claude references its contents when you ask about the project.
ⓧ I'd like to double check the full code
Here is what your CLAUDE.md file should look like:
# Task Tracker CLI
## Project overview
A Python CLI task tracker using Click. Tasks are stored as JSON in a local file.
## Tech stack
- Python 3 with type hints
- Click for CLI interface
- pytest for testing
- JSON file storage (tasks.json)
## Conventions
- Use Google-style docstrings
- Functions should have type annotations
- Keep modules small and focused (one file per concern)
- Use pathlib for file paths, not os.path
## Project structure
- task_tracker/cli.py - Click commands (entry point)
- task_tracker/storage.py - JSON file read/write
- task_tracker/models.py - Task data structures
- tests/ - pytest test files
## Commands
- Run app: python -m task_tracker
- Run tests: pytest
- Run single test file: pytest tests/test_storage.py -v
## Important
- Always store tasks with a UUID as the ID
- Task priorities are: low, medium, high
- Default priority is medium
- Date format is ISO 8601 (YYYY-MM-DD)
And your requirements.txt should contain:
click==8.4.1
pytest==9.1.0
Your environment is ready and Claude Code knows your project's rules. Next up, you will put Claude Code to work generating the actual task tracker code through natural language prompts.
Generate the Core Task Tracker with Claude Code
Your CLAUDE.md file gives Claude Code a complete picture of your project's conventions, structure, and tech stack.
In this step, you'll use Claude Code to generate the entire task tracker application through natural language prompts. You'll experience the core workflow: describe what you want, review what Claude proposes, and approve changes one file at a time.
In this step, get ready to:
- Scaffold the project structure with Claude Code.
- Implement CLI commands for adding, listing, completing, and deleting tasks.
- Commit all changes through Claude Code.
Use Claude Code to scaffold the project structure
Before Claude can build features, it needs the foundational package structure. You'll give Claude a high-level instruction and let it create each file according to your CLAUDE.md conventions.
- Start Claude Code in your project directory by running:
claude
What happens when you start Claude Code?
Claude Code loads your CLAUDE.md automatically at the start of every session. This means you never have to repeat your project conventions. Claude already knows your tech stack, file structure, and coding style.
- Once Claude's session is active, type this prompt and press Enter:
create the project structure from CLAUDE.md. Set up task_tracker/ as a package with __init__.py, models.py, storage.py, cli.py, and a __main__.py entry point. Implement the models module with a Task dataclass that has id, title, priority, completed, and created_at fields. Implement storage.py to load and save tasks to tasks.json. Don't implement CLI commands yet.
Claude will propose creating each file one at a time. It shows you the file contents and asks for approval before writing anything to disk.
- Review each proposed file as Claude presents it.
- Approve each file by typing y when prompted.
What should I see?
Claude creates files one at a time, showing you the full contents before writing. You should see it create task_tracker/__init__.py, task_tracker/models.py, task_tracker/storage.py, and tests/__init__.py.
Notice how Claude follows your CLAUDE.md conventions automatically: type hints on all functions, pathlib for file paths instead of os.path, and Google-style docstrings.
Claude asking too many questions instead of generating?
If Claude asks clarifying questions instead of creating files, answer them directly. Claude sometimes needs confirmation on design choices not covered in CLAUDE.md. You can also rephrase your prompt to be more explicit about what you want.
If Claude generates code that looks different from what you expected, that's normal. AI-generated code varies between sessions. The key is that it follows your conventions and works correctly.
Help me get Claude Code to scaffold my project
Use Claude Code to implement CLI commands
With the data layer in place, Claude can now build the CLI commands that let users interact with their tasks.
- In the same Claude Code session, type this prompt:
implement the CLI commands in cli.py using Click: add (takes title and optional --priority flag), list (shows all tasks with their status), complete (marks a task as done by ID prefix), and delete (removes a task by ID prefix). Wire up __main__.py so the app runs with python -m task_tracker.
- Review and approve each file as Claude presents its changes.
What does this prompt produce?
Claude will update cli.py with a Click command group containing four commands: add, list, complete, and delete. It will also create __main__.py so the app runs as a module.
Claude may also proactively add filtering and sorting flags to the list command based on the priority system described in your CLAUDE.md. This is normal. Claude uses your project context to anticipate useful features.
Now verify that the app actually works. Ask Claude to run it for you.
- Type this prompt to test the app through Claude Code:
run python -m task_tracker add "Learn Claude Code" --priority high, then run python -m task_tracker list to verify it shows the task
- Approve the command execution when Claude asks for permission.
What is the read-plan-edit-verify loop?
What just happened is Claude Code's core workflow pattern. Claude read your codebase, planned the implementation, edited the files, and you verified the result by running the app.
This loop is the mental model you'll use for everything in Claude Code. Describe what you want, let Claude handle the implementation details, then verify the output works.
You should see output confirming a task was added, then a list showing your task with its priority and a short ID.
Seeing a ModuleNotFoundError?
If you see an error about task_tracker not being found, make sure your virtual environment is still active. The prompt in your terminal should show (venv) at the beginning.
If the error mentions click not being found, ask Claude: "install click in the virtual environment and try again."
Help me fix my ModuleNotFoundError
Commit via Claude Code
Claude Code can also handle Git operations for you. Instead of manually staging and committing, you can describe what you want and Claude writes the commit message.
- Type this prompt in your Claude Code session:
commit all changes with a descriptive message
- Review the commit message Claude proposes and approve it.
What does Claude do with this prompt?
Claude will run git add to stage all new files, compose a descriptive commit message based on what was created, and show it to you for approval before executing git commit.
You should see Claude stage the files and present a commit message summarizing the project scaffolding. After you approve, the commit is made.
Claude not finding Git?
Make sure you initialized git init in the previous step. If Claude says the directory is not a Git repository, type: "run git init first, then commit."
Help me fix Git not being recognized
✔️ Awesome, I've got everything!
Great. Make sure you've approved all of Claude's file creations and your commit went through successfully.
ⓧ I'd like to double check the full code
Since Claude Code generates code based on your prompts, the exact output may vary slightly between sessions. Here's what each file should look like. Compare yours against these references. Note that tests/__init__.py is an empty file needed for pytest to discover tests.
"""Task Tracker CLI - A simple command-line task manager."""
"""Entry point for running task_tracker as a module."""
from task_tracker.cli import cli
if __name__ == "__main__":
cli()
"""Task data structures."""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
from uuid import uuid4
@dataclass
class Task:
"""Represents a single task.
Attributes:
id: Unique identifier for the task.
title: Description of the task.
priority: Priority level (low, medium, high).
completed: Whether the task is done.
created_at: ISO 8601 timestamp of creation.
completed_at: ISO 8601 timestamp of completion, if completed.
"""
title: str
priority: str = "medium"
completed: bool = False
id: str = field(default_factory=lambda: str(uuid4()))
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
completed_at: Optional[str] = None
def to_dict(self) -> dict:
"""Convert task to a dictionary for JSON serialization."""
return {
"id": self.id,
"title": self.title,
"priority": self.priority,
"completed": self.completed,
"created_at": self.created_at,
"completed_at": self.completed_at,
}
@classmethod
def from_dict(cls, data: dict) -> "Task":
"""Create a Task from a dictionary.
Args:
data: Dictionary containing task fields.
Returns:
A Task instance.
"""
return cls(
id=data["id"],
title=data["title"],
priority=data.get("priority", "medium"),
completed=data.get("completed", False),
created_at=data.get("created_at", datetime.now().isoformat()),
completed_at=data.get("completed_at"),
)
"""JSON file storage for tasks."""
import json
from pathlib import Path
from typing import List
from task_tracker.models import Task
DEFAULT_FILE = Path("tasks.json")
def load_tasks(filepath: Path = DEFAULT_FILE) -> List[Task]:
"""Load tasks from a JSON file.
Args:
filepath: Path to the JSON file.
Returns:
List of Task objects. Returns empty list if file does not exist.
"""
if not filepath.exists():
return []
data = json.loads(filepath.read_text())
return [Task.from_dict(item) for item in data]
def save_tasks(tasks: List[Task], filepath: Path = DEFAULT_FILE) -> None:
"""Save tasks to a JSON file.
Args:
tasks: List of Task objects to save.
filepath: Path to the JSON file.
"""
data = [task.to_dict() for task in tasks]
filepath.write_text(json.dumps(data, indent=2))
"""Click CLI commands for the task tracker."""
import click
from datetime import datetime
from typing import Optional
from task_tracker.models import Task
from task_tracker.storage import load_tasks, save_tasks
@click.group()
def cli():
"""Task Tracker - A simple command-line task manager."""
pass
@cli.command()
@click.argument("title")
@click.option("--priority", type=click.Choice(["low", "medium", "high"]), default="medium", help="Task priority level.")
def add(title: str, priority: str) -> None:
"""Add a new task."""
tasks = load_tasks()
task = Task(title=title, priority=priority)
tasks.append(task)
save_tasks(tasks)
click.echo(f"Added task: {task.title} (priority: {task.priority}, id: {task.id[:8]})")
@cli.command("list")
@click.option("--priority", type=click.Choice(["low", "medium", "high"]), default=None)
@click.option("--completed", is_flag=True, default=False)
@click.option("--pending", is_flag=True, default=False)
@click.option("--sort", "sort_by", type=click.Choice(["priority", "created"]), default=None)
def list_tasks(priority: Optional[str], completed: bool, pending: bool, sort_by: Optional[str]) -> None:
"""List all tasks with optional filtering and sorting."""
tasks = load_tasks()
if priority:
tasks = [t for t in tasks if t.priority == priority]
if completed:
tasks = [t for t in tasks if t.completed]
if pending:
tasks = [t for t in tasks if not t.completed]
if sort_by == "priority":
priority_order = {"high": 0, "medium": 1, "low": 2}
tasks.sort(key=lambda t: priority_order.get(t.priority, 1))
elif sort_by == "created":
tasks.sort(key=lambda t: t.created_at)
if not tasks:
click.echo("No tasks found.")
return
for task in tasks:
status = "done" if task.completed else "pending"
click.echo(f"[{status}] {task.title} (priority: {task.priority}, id: {task.id[:8]})")
@cli.command()
@click.argument("task_id")
def complete(task_id: str) -> None:
"""Mark a task as completed by ID prefix."""
tasks = load_tasks()
matched = [t for t in tasks if t.id.startswith(task_id)]
if not matched:
click.echo(f"No task found with ID starting with '{task_id}'.")
return
if len(matched) > 1:
click.echo(f"Multiple tasks match '{task_id}'. Be more specific.")
return
task = matched[0]
task.completed = True
task.completed_at = datetime.now().isoformat()
save_tasks(tasks)
click.echo(f"Completed: {task.title}")
@cli.command()
@click.argument("task_id")
def delete(task_id: str) -> None:
"""Delete a task by ID prefix."""
tasks = load_tasks()
matched = [t for t in tasks if t.id.startswith(task_id)]
if not matched:
click.echo(f"No task found with ID starting with '{task_id}'.")
return
if len(matched) > 1:
click.echo(f"Multiple tasks match '{task_id}'. Be more specific.")
return
task = matched[0]
tasks.remove(task)
save_tasks(tasks)
click.echo(f"Deleted: {task.title}")
Your task tracker is alive. You described what you wanted in plain English, and Claude Code turned it into a working Python application. Next up, you'll learn how to use plan mode to think through a complex feature before Claude touches any code.
Use Plan Mode to Add Priorities and Filtering
Your task tracker has basic CRUD operations, but the list command dumps every task with no way to filter or sort them. As your task list grows, you need a way to slice through the noise.
This is where plan mode shines. Plan mode separates thinking from doing. Claude reads your codebase and proposes changes, but makes no edits until you explicitly approve. You get to review the architecture before a single line changes.
In this step, get ready to:
- Enter plan mode and have Claude design a filtering feature.
- Review the plan, approve it, and let Claude implement.
- Test the filtering flags manually and commit.
Enter plan mode and ask Claude to plan a filtering feature
In normal mode, Claude reads, plans, and edits in one go. Plan mode adds a gate: Claude proposes changes and waits for your approval before touching any files.
- Start a Claude Code session in plan mode by running this command:
claude --permission-mode plan
What is plan mode?
Plan mode restricts Claude to read-only operations. It can analyze your files and propose a detailed plan, but it cannot create, edit, or delete anything until you switch it back to full mode or tell it to go ahead.
This is especially useful for complex features where you want to validate the approach before committing to an implementation.
- Once Claude Code starts, type this prompt to describe what you want:
I want to add filtering to the list command. Users should be able to filter by priority (--priority high), by status (--completed/--pending), or combine both. Also add a --sort flag that sorts by priority or created date. Plan the changes but don't edit yet.
What should I see?
Claude will read your project files (especially cli.py and CLAUDE.md) and produce a plan. The plan will outline which files need changes, what the new CLI flags look like, and how the filtering logic works.
If Claude sees that the basic structure is already in place, it may propose enhancements like adding help strings or refining the implementation. This is normal. Review the plan carefully before approving.
- Read through Claude's proposed plan. If something looks off, push back or ask clarifying questions before approving.
Push back on plans you disagree with
Plan mode is a conversation, not a one-shot answer. If Claude's plan introduces unnecessary complexity or misses a requirement, tell it. Steering the plan is the core skill this step teaches.
Approve the plan and let Claude implement
Once you are satisfied with Claude's plan, you need to give it permission to execute the changes.
- Tell Claude to implement the plan by typing this prompt:
the plan looks good, implement it
What happens next?
Claude will execute the changes it outlined. Watch how it modifies only the files it proposed, following the structure it described. It will ask for your approval before writing each file.
This is the read-plan-edit-verify loop in action: Claude read the codebase, planned the approach, and now edits files one at a time with your sign-off.
- Review each file change as Claude presents it and approve them.
Test the filtering manually and commit
With the filtering implemented, you need to verify it works by adding test data and exercising the new flags.
- Ask Claude to create test tasks and run the filtering commands:
add a few test tasks with different priorities, then test the filtering flags: list --priority high, list --pending, list --sort priority
What should I see?
Claude will run each command and show you the output. You should see tasks filtered to only high-priority items with --priority high, only incomplete tasks with --pending, and tasks ordered from high to low priority with --sort priority.
- Once you have confirmed the filtering works, ask Claude to commit:
commit these changes with a message about adding filtering and sorting
- Claude will stage the changed files, write a commit message, and show it to you for approval.
- Approve the commit. Then exit the session with /exit or Ctrl+D.
Filtering not working as expected?
If --priority high returns no tasks, make sure you added tasks with priority high in the previous prompt. Claude may have created all tasks with the default medium priority.
If Claude seems stuck or the output looks wrong, try telling it the exact symptom you see and ask it to investigate.
Help me debug why my filtering flags are not returning the expected tasks.
✔️ Awesome, I've got everything!
Great. Double check that you have saved your files and committed via Claude Code.
ⓧ I'd like to double check the full code
Here is what task_tracker/cli.py should look like after Claude implements the filtering enhancements:
"""Click CLI commands for the task tracker."""
import click
from datetime import datetime
from typing import Optional
from task_tracker.models import Task
from task_tracker.storage import load_tasks, save_tasks
@click.group()
def cli():
"""Task Tracker - A simple command-line task manager."""
pass
@cli.command()
@click.argument("title")
@click.option("--priority", type=click.Choice(["low", "medium", "high"]), default="medium", help="Task priority level.")
def add(title: str, priority: str) -> None:
"""Add a new task."""
tasks = load_tasks()
task = Task(title=title, priority=priority)
tasks.append(task)
save_tasks(tasks)
click.echo(f"Added task: {task.title} (priority: {task.priority}, id: {task.id[:8]})")
@cli.command("list")
@click.option("--priority", type=click.Choice(["low", "medium", "high"]), default=None, help="Filter by priority.")
@click.option("--completed", is_flag=True, default=False, help="Show only completed tasks.")
@click.option("--pending", is_flag=True, default=False, help="Show only pending tasks.")
@click.option("--sort", "sort_by", type=click.Choice(["priority", "created"]), default=None, help="Sort tasks.")
def list_tasks(priority: Optional[str], completed: bool, pending: bool, sort_by: Optional[str]) -> None:
"""List all tasks with optional filtering and sorting."""
tasks = load_tasks()
if priority:
tasks = [t for t in tasks if t.priority == priority]
if completed:
tasks = [t for t in tasks if t.completed]
if pending:
tasks = [t for t in tasks if not t.completed]
if sort_by == "priority":
priority_order = {"high": 0, "medium": 1, "low": 2}
tasks.sort(key=lambda t: priority_order.get(t.priority, 1))
elif sort_by == "created":
tasks.sort(key=lambda t: t.created_at)
if not tasks:
click.echo("No tasks found.")
return
for task in tasks:
status = "done" if task.completed else "pending"
click.echo(f"[{status}] {task.title} (priority: {task.priority}, id: {task.id[:8]})")
@cli.command()
@click.argument("task_id")
def complete(task_id: str) -> None:
"""Mark a task as completed by ID prefix."""
tasks = load_tasks()
matched = [t for t in tasks if t.id.startswith(task_id)]
if not matched:
click.echo(f"No task found with ID starting with '{task_id}'.")
return
if len(matched) > 1:
click.echo(f"Multiple tasks match '{task_id}'. Be more specific.")
return
task = matched[0]
task.completed = True
task.completed_at = datetime.now().isoformat()
save_tasks(tasks)
click.echo(f"Completed: {task.title}")
@cli.command()
@click.argument("task_id")
def delete(task_id: str) -> None:
"""Delete a task by ID prefix."""
tasks = load_tasks()
matched = [t for t in tasks if t.id.startswith(task_id)]
if not matched:
click.echo(f"No task found with ID starting with '{task_id}'.")
return
if len(matched) > 1:
click.echo(f"Multiple tasks match '{task_id}'. Be more specific.")
return
task = matched[0]
tasks.remove(task)
save_tasks(tasks)
click.echo(f"Deleted: {task.title}")
You just experienced the plan mode workflow from start to finish. Next up, you will use Claude Code to debug a real error and watch its diagnostic process in action.
Debug a Bug with Claude Code
You have a working task tracker with filtering, sorting, and a clean Git history. In real projects, bugs appear when you least expect them.
In this step, you will simulate a realistic debugging scenario. You will intentionally break your code, then hand the problem to Claude Code and watch how it diagnoses and fixes the issue from nothing more than a symptom description.
In this step, get ready to:
- Introduce a deliberate bug into storage.py.
- Let Claude Code diagnose the bug from a symptom description.
- Verify the fix and commit it.
Introduce a deliberate bug
The load_tasks function in storage.py currently handles the case where tasks.json does not exist yet. You will remove that safety check to simulate a real bug that crashes the app on a fresh install.
- Exit your Claude Code session if you still have one open by pressing Ctrl+D or typing /exit.
- Open storage.py in a text editor by running this command:
nano task_tracker/storage.py
- Find these two lines inside the load_tasks function:
if not filepath.exists():
return []
- Delete both of those lines so the function jumps straight to reading the file.
- Also delete the blank line that was between the check and the data = line.
- Save and exit nano by pressing Ctrl+X, then Y, then Enter.
After your edit, the load_tasks function should look like this:
def load_tasks(filepath: Path = DEFAULT_FILE) -> List[Task]:
"""Load tasks from a JSON file.
Args:
filepath: Path to the JSON file.
Returns:
List of Task objects. Returns empty list if file does not exist.
"""
data = json.loads(filepath.read_text())
return [Task.from_dict(item) for item in data]
Why introduce a bug on purpose?
In a real project, you would not know exactly where a bug lives. You would only see a symptom (a crash, wrong output, or unexpected behavior). By breaking the code yourself, you can practice Claude Code's debugging workflow in a controlled setting where you already know the answer.
Ask Claude Code to find and fix the bug
Now you will hand the problem to Claude Code using only a symptom description. You will not tell it which file is broken or what the fix looks like.
- Start Claude Code by running:
claude
- Describe the symptom by typing this prompt into the Claude Code session:
I'm getting a FileNotFoundError when I run "python -m task_tracker list" on a fresh install with no tasks.json file. Find the bug and fix it.
Watch Claude's debugging process unfold. It will read the error (or reproduce it by running the command), trace through storage.py to find the missing file check, propose a fix, and ask for your approval to apply it.
- Review the fix Claude proposes. It should restore the file-existence check you removed.
- Approve the change when Claude asks for permission.
What is the debugging workflow?
This is the pattern you will use for all future debugging with Claude Code: describe the symptom, let Claude investigate, review its proposed fix, then approve or push back. You never need to pinpoint the exact file or line yourself.
Claude reads relevant files, traces the logic, and narrows down the cause. Your job is to verify the fix makes sense before approving it.
Verify the fix and commit
A fix is only confirmed when you can reproduce the original failure scenario and see it pass. You will have Claude delete the data file and re-run the command to prove the fix holds.
- Ask Claude to verify the fix and commit by typing this prompt:
delete tasks.json, then run python -m task_tracker list to confirm it handles the missing file gracefully. Then commit the fix.
- Watch Claude execute the verification. You should see output showing No tasks found. instead of a traceback error.
- Approve the commit when Claude stages the fix and shows you the commit message.
What just happened?
Claude completed the full read-plan-edit-verify loop in one exchange. It read the codebase, identified the missing guard clause, edited storage.py to restore the check, then verified the fix by running the command itself.
Claude didn't find the bug?
If Claude suggests an unrelated fix or gets confused, provide more detail. Mention that the error is a FileNotFoundError in storage.py and that it only happens when tasks.json is missing. You can also ask Claude to run the failing command so it sees the traceback directly.
Help me figure out why Claude Code isn't finding my storage.py bug
✔️ Awesome, I've got everything!
Great. Confirm that storage.py is back to its correct state with the file-existence check restored.
ⓧ I'd like to double check the full code
Here is what task_tracker/storage.py should look like after Claude's fix:
"""JSON file storage for tasks."""
import json
from pathlib import Path
from typing import List
from task_tracker.models import Task
DEFAULT_FILE = Path("tasks.json")
def load_tasks(filepath: Path = DEFAULT_FILE) -> List[Task]:
"""Load tasks from a JSON file.
Args:
filepath: Path to the JSON file.
Returns:
List of Task objects. Returns empty list if file does not exist.
"""
if not filepath.exists():
return []
data = json.loads(filepath.read_text())
return [Task.from_dict(item) for item in data]
def save_tasks(tasks: List[Task], filepath: Path = DEFAULT_FILE) -> None:
"""Save tasks to a JSON file.
Args:
tasks: List of Task objects to save.
filepath: Path to the JSON file.
"""
data = [task.to_dict() for task in tasks]
filepath.write_text(json.dumps(data, indent=2))
You just experienced Claude Code's diagnostic workflow from start to finish. Next up, you will have Claude Code write an entire test suite for your project and fix any failures automatically.
Have Claude Code Write Tests
You just used Claude Code to hunt down and fix a bug. But how do you know your fix didn't quietly break something else?
Automated tests answer that question. In this step, you'll ask Claude Code to write a full pytest test suite, then have it run and fix any failures in a single prompt. This demonstrates one of Claude Code's most powerful patterns: iterating until the tests pass without you lifting a finger.
In this step, get ready to:
- Have Claude Code generate tests for the storage module and CLI commands.
- Run the tests and let Claude Code fix any failures automatically.
- Commit the test suite to Git via Claude Code.
Ask Claude to write a test suite
Writing tests by hand is often the most tedious part of development. Claude Code can generate comprehensive test suites that follow your project's conventions because it reads your CLAUDE.md and understands your codebase structure.
- In your Claude Code session, type this prompt to request a full test suite:
write a comprehensive test suite for this project using pytest. Cover the storage module (loading, saving, handling missing file) and the CLI commands (add, list, complete, delete, filtering). Use a temporary directory for test isolation so tests don't affect real task data.
What will Claude do with this prompt?
Claude will read your existing code, then create test files in the tests/ directory following the pytest conventions from your CLAUDE.md.
It will use pytest fixtures like tmp_path for test isolation. This means each test runs in its own temporary directory, so no test can accidentally pollute your real tasks.json file.
- Review the proposed changes as Claude creates each file. Approve them one at a time to understand what Claude is generating.
Claude should create two test files: tests/test_storage.py and tests/test_cli.py.
What should the tests cover?
For test_storage.py: loading from a missing file returns an empty list, tasks survive a save/load round-trip, saved files contain valid JSON, and completed status is preserved.
For test_cli.py: adding a task, listing tasks, completing a task, deleting a task, filtering by priority, and filtering by pending status.
Run tests and fix any failures
Here's one of Claude Code's most powerful patterns: you can ask it to run tests AND fix any failures in a single prompt. Claude will iterate until everything passes, showing you each fix along the way.
- Ask Claude Code to run and fix the tests by typing this prompt:
run pytest -v and fix any failures
What does this pattern unlock?
This is the iterate-until-green pattern. Claude runs pytest -v, reads the output, identifies any failing tests, traces the root cause, applies a fix, and re-runs the suite. It repeats this loop until all tests pass.
You still review each fix before it's applied. The value is that Claude does the diagnosis and iteration automatically instead of you manually reading tracebacks.
Once Claude finishes iterating, you should see all tests passing with green output.
Tests still failing after Claude's fixes?
If Claude gets stuck in a loop or introduces new failures, tell it to stop and re-read the error traceback more carefully. Sometimes a follow-up prompt like "focus on the assertion error in test_list_filter_pending" gives Claude enough direction to resolve a specific issue.
help me debug failing pytest tests in Claude Code
Commit the test suite
With all tests passing, it's time to lock in this progress with a commit.
- Ask Claude Code to commit by typing this prompt:
commit the test suite with a descriptive message
- Review the commit message Claude proposes and approve it.
Why let Claude write commit messages?
Claude reads the staged diff and writes a message that describes the actual changes. This saves you the mental overhead of summarizing your own work, and the messages are often more detailed than what you'd write in a rush.
You should see Claude stage the test files and create a commit.
Commit failed or nothing to commit?
If Claude says there's nothing to commit, the test files may already have been committed during an earlier step. Ask Claude to run git status to check. If the working tree is clean, your tests are already safely committed.
help me resolve a Git commit issue in Claude Code
✔️ Awesome, I've got everything!
Great. Double check you have both tests/test_storage.py and tests/test_cli.py in your project.
ⓧ I'd like to double check the full code
Here is what your tests/test_storage.py should look like:
"""Tests for the storage module."""
import json
from pathlib import Path
import pytest
from task_tracker.models import Task
from task_tracker.storage import load_tasks, save_tasks
@pytest.fixture
def tmp_tasks_file(tmp_path: Path) -> Path:
"""Provide a temporary file path for task storage."""
return tmp_path / "tasks.json"
def test_load_tasks_missing_file(tmp_tasks_file: Path) -> None:
"""Loading from a non-existent file returns an empty list."""
result = load_tasks(tmp_tasks_file)
assert result == []
def test_save_and_load_tasks(tmp_tasks_file: Path) -> None:
"""Tasks survive a round-trip through save and load."""
tasks = [
Task(title="First task", priority="high"),
Task(title="Second task", priority="low"),
]
save_tasks(tasks, tmp_tasks_file)
loaded = load_tasks(tmp_tasks_file)
assert len(loaded) == 2
assert loaded[0].title == "First task"
assert loaded[0].priority == "high"
assert loaded[1].title == "Second task"
def test_save_creates_valid_json(tmp_tasks_file: Path) -> None:
"""Saved file contains valid JSON."""
tasks = [Task(title="JSON test")]
save_tasks(tasks, tmp_tasks_file)
data = json.loads(tmp_tasks_file.read_text())
assert isinstance(data, list)
assert data[0]["title"] == "JSON test"
def test_load_preserves_completed_status(tmp_tasks_file: Path) -> None:
"""Completed status is preserved through save/load cycle."""
task = Task(title="Done task", completed=True, completed_at="2026-06-15T10:00:00")
save_tasks([task], tmp_tasks_file)
loaded = load_tasks(tmp_tasks_file)
assert loaded[0].completed is True
assert loaded[0].completed_at == "2026-06-15T10:00:00"
And here is what your tests/test_cli.py should look like:
"""Tests for the CLI commands."""
from pathlib import Path
from unittest.mock import patch
import pytest
from click.testing import CliRunner
from task_tracker.cli import cli
from task_tracker.storage import DEFAULT_FILE
@pytest.fixture
def runner(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> CliRunner:
"""Provide a CLI runner with isolated task storage."""
monkeypatch.chdir(tmp_path)
return CliRunner()
def test_add_task(runner: CliRunner) -> None:
result = runner.invoke(cli, ["add", "Test task"])
assert result.exit_code == 0
assert "Added task: Test task" in result.output
def test_add_task_with_priority(runner: CliRunner) -> None:
result = runner.invoke(cli, ["add", "Urgent", "--priority", "high"])
assert result.exit_code == 0
assert "priority: high" in result.output
def test_list_empty(runner: CliRunner) -> None:
result = runner.invoke(cli, ["list"])
assert result.exit_code == 0
assert "No tasks found" in result.output
def test_list_shows_tasks(runner: CliRunner) -> None:
runner.invoke(cli, ["add", "My task"])
result = runner.invoke(cli, ["list"])
assert result.exit_code == 0
assert "My task" in result.output
assert "pending" in result.output
def test_complete_task(runner: CliRunner) -> None:
runner.invoke(cli, ["add", "Finish me"])
list_result = runner.invoke(cli, ["list"])
task_id = list_result.output.split("id: ")[1][:8]
result = runner.invoke(cli, ["complete", task_id])
assert result.exit_code == 0
assert "Completed: Finish me" in result.output
def test_delete_task(runner: CliRunner) -> None:
runner.invoke(cli, ["add", "Delete me"])
list_result = runner.invoke(cli, ["list"])
task_id = list_result.output.split("id: ")[1][:8]
result = runner.invoke(cli, ["delete", task_id])
assert result.exit_code == 0
assert "Deleted: Delete me" in result.output
list_after = runner.invoke(cli, ["list"])
assert "Delete me" not in list_after.output
def test_list_filter_by_priority(runner: CliRunner) -> None:
runner.invoke(cli, ["add", "High task", "--priority", "high"])
runner.invoke(cli, ["add", "Low task", "--priority", "low"])
result = runner.invoke(cli, ["list", "--priority", "high"])
assert "High task" in result.output
assert "Low task" not in result.output
def test_list_filter_pending(runner: CliRunner) -> None:
runner.invoke(cli, ["add", "Pending task"])
runner.invoke(cli, ["add", "Done task"])
list_result = runner.invoke(cli, ["list"])
task_id = list_result.output.split("Done task")[1].split("id: ")[1][:8]
runner.invoke(cli, ["complete", task_id])
result = runner.invoke(cli, ["list", "--pending"])
assert "Pending task" in result.output
assert "Done task" not in result.output
Your Claude-generated tests may differ slightly in variable names or docstrings. The important thing is that the same scenarios are covered and all tests pass.
Secret mission
Add a Stats Command
Your task tracker works, but it doesn't tell you how productive you've been. Add a stats command that shows completion metrics and a daily streak. The twist: you'll give Claude a vague prompt and practice steering it with follow-ups instead of spelling everything out upfront.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs beyond your existing Claude Pro subscription at $20/month.
Decide whether to keep your resources for future experimentation or delete them entirely.
Resources you used:
- Project files in ~/task-tracker/ (local directory with Python source, tests, and virtual environment).
- A tasks.json data file (local, created when you add tasks).
- Claude Code CLI installed at ~/.local/bin/claude with data in ~/.local/share/claude.
- Claude Code configuration files at ~/.claude/ (session history, settings).
Keep everything running
No action needed. Choose this if you want to keep experimenting with Claude Code workflows on this project or use your CLAUDE.md as a template for future Python projects.
- Your ~/task-tracker/ directory stays on disk and is ready to use any time.
- Claude Code remains installed and will auto-update in the background.
- Your Claude Pro subscription continues at its normal rate regardless of this project.
Pause - I'll come back to this later
Deactivate the virtual environment to free up your shell. The project files stay on disk for later.
- Deactivate the virtual environment by running:
deactivate
- When you come back, reactivate with:
cd ~/task-tracker
source venv/bin/activate
Delete - I don't want to use this again
Remove all project files, the Claude Code CLI, and its configuration. This fully cleans your system of everything this project created.
- Deactivate the virtual environment first (if still active):
deactivate
- Delete the project directory:
rm -rf ~/task-tracker
- Remove the Claude Code binary and its data:
rm -f ~/.local/bin/claude
rm -rf ~/.local/share/claude
- Remove Claude Code configuration and session history:
rm -rf ~/.claude
rm -f ~/.claude.json
Nice Work!
Nice Work!
You just built a fully functional Python CLI task tracker, and you barely typed any of the application code yourself. More importantly, you now have the muscle memory for Claude Code's core workflows.
Here's what you learned:
- Configured a CLAUDE.md project file that teaches Claude Code your conventions, then used agentic code generation to scaffold and implement a full Click-based CLI app through natural language.
- Used plan mode to separate thinking from doing, reviewing Claude's approach to filtering and sorting before approving implementation.
- Practiced the read-plan-edit-verify loop for debugging and test generation, letting Claude Code diagnose errors and write a full pytest suite while fixing failures automatically.
- Secret Mission: Added a stats command with streak tracking, practicing the art of steering Claude with follow-up prompts rather than specifying everything upfront.
Ready to quiz yourself?