Build a Task Tracker with Claude Code

Build a Python CLI task tracker using AI-assisted development with Claude Code.

Introduction

30 Second Summary

Imagine having a coding partner who can write, test, and commit code on your behalf. All you have to do is describe what you want in plain English.

In this project, you will use Claude Code to build a complete Python command-line task tracker without writing any code yourself. You'll learn the professional workflow for directing an AI coding agent: exploring, planning, implementing, and verifying.

What You'll Build

A Python command-line task tracker application, built entirely by directing Claude Code with natural language prompts.

By the end of this project, you'll have:

  • A CLAUDE.md file that gives Claude persistent project context, acting as your project's memory.
  • A working CLI app with add, list, complete, and delete functionality, plus priority filtering.
  • Automated tests that Claude wrote and ran to verify its own work, along with a clean Git history of Claude-authored commits.
  • Secret Mission: Create a custom slash command that automates your code review workflow.

Are there any prerequisites?

You'll need basic familiarity with using a terminal or command line. No prior Claude Code experience is needed.

Claude Code requires a Claude Pro ($20/month), Max ($100/month), or Console account with pre-paid API credits. A free Claude.ai plan does not include Claude Code access.

Install Claude Code and Prerequisites

Claude Code is a command-line tool that runs directly in your terminal. It reads your files, executes commands, and makes changes to your project on your behalf.

To use it, you need three things installed: Git (which gives Claude Code access to better shell tools), Python 3 (for the app you'll build), and Claude Code itself.

In this step, get ready to:
  • Install Git for Windows and Python 3.
  • Install Claude Code via PowerShell.
  • Authenticate with your Claude account.
Install Git for Windows and Python 3

Claude Code works best on Windows when Git for Windows is installed. Git for Windows provides a Bash shell that Claude Code uses to run commands more reliably.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.

You have two options for installing Git for Windows. You can either download the installer from the official website, or use the Windows package manager in PowerShell.

  • Install Git for Windows by running this command in PowerShell:
winget install --id Git.Git -e --source winget

What does this command do?

This uses Windows Package Manager (winget) to download and install Git for Windows automatically. The --id Git.Git flag specifies the exact package to install.

  • After the installation finishes, close PowerShell completely and reopen it. This ensures your system recognizes the new git command.
  • Verify Git is installed by running:
git --version

✔️ I see a version number

Git is ready to go. You should see something like git version 2.x.x.

ⓧ Command not found

PowerShell does not recognize the git command. Try these fixes:

  • Make sure you closed and reopened PowerShell after installing Git.
  • If you still see the error, download the installer directly from https://git-scm.com. Run the downloaded installer and accept the default options.
  • After installing, close and reopen PowerShell again, then retry git --version.

Now verify that Python 3 is installed on your machine.

  • Check your Python installation by running:
python --version

✔️ I see a version starting with 3

Python is ready. You should see something like Python 3.x.x. Any Python 3 version works for this project.

ⓧ Command not found or the Microsoft Store opens

Windows does not have Python installed by default. Install it now:

  • Go to https://www.python.org/downloads/ in your browser.
  • Click the large Download Python 3 button at the top of the page.
  • Run the downloaded installer.
  • On the first installer screen, check the box labeled Add python.exe to PATH. This is critical.
  • Click Install Now.
  • After installation completes, close and reopen PowerShell.
  • Run python --version again. You should now see a version number starting with 3.

Why Git for Windows?

Git for Windows bundles a Bash shell. Claude Code uses this Bash shell to run commands more effectively than it can with PowerShell alone. Without Git installed, Claude Code falls back to PowerShell, which works but is less capable.

Install Claude Code using PowerShell

Claude Code has a native Windows installer that you run directly in PowerShell. It downloads and sets up everything automatically.

  • Install Claude Code by running this command in PowerShell:
irm https://claude.ai/install.ps1 | iex

What does this command do?

The irm command (short for Invoke-RestMethod) downloads the install script from Anthropic's servers. The pipe (|) sends it to iex (Invoke-Expression), which runs the script. This is Anthropic's official installation method for Windows.

  • After installation completes, verify Claude Code is installed by running:
claude --version
  • You should see a version number printed in the terminal. Now run a full health check:
claude doctor

This command checks that Claude Code can find Git, Python, and your network connection. It reports any issues that might prevent Claude Code from working correctly.

Claude Code command not recognized?

  • Close PowerShell completely and reopen it. The installation adds Claude Code to your PATH, but the current session might not see it yet.
  • If it still fails, try running the install command again. The native installation automatically updates in the background, so re-running is safe.

Help me troubleshoot my Claude Code installation on Windows.

Authenticate Claude Code

Claude Code needs to connect to your Anthropic account to work. The first time you launch it, it opens your browser for authentication.

What account do I need?

Claude Code requires a Claude Pro, Max, Team, Enterprise, or Console account. The free Claude.ai plan does not include Claude Code access.

  • Start Claude Code by running:
claude
  • Claude Code will open your default browser and ask you to log in with your Anthropic account. Follow the prompts to authorize access.
  • Once authenticated, you will see the Claude Code welcome screen back in your terminal. This confirms the connection is working.
  • Type exit and press Enter to close the session for now.

Browser didn't open?

  • If the browser does not open automatically, look for a URL printed in the terminal. Copy and paste it into your browser manually.
  • Make sure you are logging in with an account that has Claude Code access (Pro, Max, Team, Enterprise, or Console).

Help me authenticate Claude Code on Windows.

Your machine is fully set up with Git, Python, and Claude Code. Next up, you will create your project folder and teach Claude about what you are building.

Initialize Your Project and Create CLAUDE.md

In the previous step, you installed Claude Code and confirmed it can authenticate. Now it's time to create your project and give Claude a "brain" for it.

Claude Code reads a special file called CLAUDE.md at the start of every session. This file holds your project's context: what you're building, how to run it, and what rules to follow. The better your CLAUDE.md, the smarter Claude's suggestions become.

In this step, get ready to:
  • Create your project directory and initialize a Git repository.
  • Create a CLAUDE.md file that gives Claude persistent project context.
  • Verify that Claude reads your CLAUDE.md correctly in a fresh session.
Create a new project directory and initialize Git

Every Claude Code project lives in a folder with a Git repository. Git tracks your changes so Claude Code can create meaningful commits later.

  • In PowerShell, move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder, move into it, and initialize a Git repository by running these commands:
mkdir task-tracker
cd task-tracker
git init
  • Confirm the folder and Git repository were created by running:
git status

You should see a message saying you're on a branch with no commits yet. This confirms Git is ready.

Seeing 'git' is not recognized?

This means Git for Windows isn't on your system PATH. Close PowerShell completely and reopen it. The installer sometimes requires a restart of the terminal to pick up the new PATH entries.

Help me fix my Git installation

  • Open a Claude Code session inside your project by running:
claude

You should see the Claude Code welcome screen appear in your terminal. You're now inside an interactive Claude Code session, ready to give instructions.

Claude Code won't start?

Make sure you're inside the task-tracker folder before running claude. If you see an authentication error, run /login inside the session to re-authenticate.

Help me troubleshoot Claude Code startup issues

Create your CLAUDE.md file

CLAUDE.md is a special Markdown file that Claude reads at the start of every conversation. It acts as persistent memory for your project, telling Claude what you're building, what tools you use, and what rules to follow.

  • Inside your Claude Code session, generate a starter CLAUDE.md by typing this command:
/init

What does /init do?

The /init command analyzes your current project structure to detect build systems, test frameworks, and code patterns. It then generates a CLAUDE.md file with what it discovers.

Since your project is empty right now, the starter file will be minimal. You'll refine it next with specific instructions for your task tracker.

  • Claude will ask you to approve creating the file. Accept the proposed changes.
  • Now refine the CLAUDE.md by giving Claude this prompt:
Update CLAUDE.md for a Python CLI task tracker project. Include: an overview (command-line task tracker with add, list, complete, delete operations, storing tasks in a JSON file), tech stack (Python 3, pytest for testing, JSON file storage), project structure (task_tracker/ package with cli.py, models.py, storage.py, plus tests/ directory and tasks.json data file), commands to run the app (python -m task_tracker) and tests (pytest, pytest -v), coding conventions (type hints on all function signatures, docstrings for all public functions, graceful error handling, small focused functions), and important rules (never commit tasks.json, always run pytest after changes, use argparse for CLI handling).

Why be so specific in this prompt?

The more precise your instructions, the better Claude's output. By spelling out the exact structure, conventions, and rules upfront, you prevent Claude from guessing or making assumptions later when it builds the actual code.

Think of CLAUDE.md as a briefing document for a new team member. Everything Claude needs to know about how this project works goes here.

  • Review the file Claude generates. Accept the changes when you're satisfied.
  • Verify the file exists by asking Claude:
Show me the contents of CLAUDE.md

Claude should display the full contents of your CLAUDE.md. Confirm it includes sections for Overview, Tech Stack, Project Structure, Commands, Coding Conventions, and Important Rules.

Your file doesn't need to match word-for-word

Claude may phrase things slightly differently from the reference below. What matters is that all the key sections are present: overview, tech stack, project structure, commands, coding conventions, and important rules. If a section is missing, ask Claude to add it.

✔️ Awesome, I've got everything!

Great. Make sure your CLAUDE.md covers all six sections. You'll use this file for the rest of the project.

ⓧ I'd like to double check the full code

Here's the reference version of CLAUDE.md. Your file should contain similar content, even if the exact wording varies:

# Task Tracker CLI

## Overview
A command-line task tracker built with Python. Manages tasks with add, list, complete, and delete operations. Tasks are stored in a local JSON file.

## Tech Stack
- Python 3
- pytest for testing
- JSON file storage (tasks.json)

## Project Structure
- `task_tracker/` - Main package
- `task_tracker/cli.py` - CLI entry point and argument parsing
- `task_tracker/models.py` - Task data model
- `task_tracker/storage.py` - JSON file read/write operations
- `tests/` - Test directory
- `tasks.json` - Data file (created at runtime, not committed)

## Commands
- Run the app: `python -m task_tracker`
- Run tests: `pytest`
- Run tests with output: `pytest -v`

## Coding Conventions
- Use type hints on all function signatures
- Write docstrings for all public functions
- Handle errors gracefully with user-friendly messages
- Keep functions small and focused (single responsibility)

## Important Rules
- Never commit tasks.json (it contains user data)
- Always run pytest after making changes
- Use argparse for CLI argument handling
Verify Claude reads your CLAUDE.md

The real test of a good CLAUDE.md is whether Claude can describe your project without you explaining anything. A fresh session forces Claude to rely entirely on what's in the file.

  • Exit your current Claude Code session by typing:
exit
  • Start a fresh Claude Code session by running:
claude

Why start a fresh session?

Claude Code loads CLAUDE.md into its context at the start of every new conversation. By exiting and re-entering, you're testing that the file loads correctly from scratch, without any leftover context from the previous session.

  • Ask Claude to describe your project by typing:
What is this project?

Claude should describe your task tracker based on the CLAUDE.md context. It should mention Python, CLI task management, JSON storage, and pytest. This confirms your project memory is working.

Claude doesn't know about the project?

Make sure you're running claude from inside the task-tracker directory. Claude Code looks for CLAUDE.md in your current working directory. If you're in a different folder, it won't find the file.

Help me figure out why Claude isn't reading my CLAUDE.md

Your project now has a persistent brain. Every time you start a Claude Code session in this folder, Claude will read your CLAUDE.md and know exactly what you're building. Next up, you'll use this context to plan your entire application before writing a single line of code.

Plan Your Application with Plan Mode

You have a CLAUDE.md giving Claude persistent context about your task tracker project. Now it's time to architect the application before any code gets written.

Professional developers using Claude Code follow a specific workflow: explore first, plan second, implement third. This prevents Claude from jumping straight into code that might not match your vision.

In this step, get ready to:
  • Ask Claude to create a detailed implementation plan.
  • Refine the plan by challenging Claude's assumptions.
  • Approve the plan and watch Claude implement the full application.
Ask Claude to create an implementation plan

Instead of telling Claude to start coding immediately, you'll ask it to plan first. This gives you a chance to review the architecture and catch issues before any code exists.

  • In your Claude Code session, type this prompt and press Enter:
Plan a Python CLI task tracker with add, list, complete, and delete commands. Tasks should be stored in a JSON file. Do not write any code yet. Just give me the plan.

What does this prompt do?

The key phrase here is "Do not write any code yet." This tells Claude to stay in planning mode. Without it, Claude would jump straight into creating files.

Claude will respond with a structured plan covering file structure, data model, and CLI interface design. Take a moment to read through it.

  • Review the plan Claude produces. Look for three things: the file structure (which files it will create), the data model (how a task is represented), and the CLI interface (what commands the user can run).

Why plan before coding?

This is the officially recommended workflow from Anthropic's best practices. Planning first reduces hallucinations and produces cleaner implementations.

It also teaches the mental model professionals use with Claude Code: explore, plan, implement, verify, ship.

Refine the plan by asking questions

A good plan deserves scrutiny. Before giving Claude the green light, challenge its assumptions to make sure the design is solid.

  • Ask Claude follow-up questions to probe its design decisions. Here are some good ones to try:
Why JSON instead of SQLite?
  • After Claude answers, try another:
How will you handle task IDs?
  • And one more to test edge case thinking:
What happens if the JSON file doesn't exist yet?

What is the explore phase?

This is the "explore" phase of the Claude Code workflow. Claude answers your questions and refines its thinking without making any changes to your project.

You're building shared understanding. The more Claude knows about your expectations, the better its implementation will be.

Ask your own questions too

The three questions above are suggestions. If something in Claude's plan doesn't make sense to you, ask about it. There are no wrong questions in the explore phase.

Approve the plan and tell Claude to proceed

Once you're satisfied with Claude's answers, it's time to switch from planning to implementation. One prompt is all it takes.

  • Give Claude the green light by typing:
The plan looks good. Go ahead and implement it.

What happens next?

Claude will start creating files and writing code. You'll see it propose changes to your project. Claude Code asks for permission before modifying your filesystem, so you can review each file change before accepting it.

Accept the changes as Claude proposes them. It will create the task_tracker/ package with multiple Python files, plus a .gitignore file.

  • Watch as Claude creates the project structure. It should create these files:
  • task_tracker/__init__.py (package marker).
  • task_tracker/models.py (Task data model).
  • task_tracker/storage.py (JSON file read/write).
  • task_tracker/cli.py (CLI entry point and argument parsing).
  • task_tracker/__main__.py (allows running with python -m task_tracker).
  • .gitignore (excludes tasks.json and cache files).

Once Claude finishes implementing, verify the files exist by running this in your Claude Code session:

List all files in this project

Claude should show you the full file tree including all six files listed above.

Claude created different files?

Claude Code is non-deterministic. It may name files slightly differently or organize code in a different structure. As long as you have a models file, a storage file, a CLI file, and a package entry point, you're in good shape.

If Claude created something drastically different (like a single-file script instead of a package), ask it to restructure: "Reorganize this into a proper Python package with separate files for models, storage, and CLI."

Claude didn't create the files I expected. How do I get it to restructure?

✔️ Awesome, I've got everything!

Great. Your project structure is ready and Claude has implemented the full task tracker application.

ⓧ I'd like to double check the full code

Here's what each file should look like. Compare these with what Claude generated.

task_tracker/__init__.py:

"""Task Tracker CLI - A simple command-line task management tool."""

task_tracker/models.py:

"""Task data model."""

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum


class Priority(Enum):
    """Task priority levels."""

    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"


@dataclass
class Task:
    """Represents a single task."""

    id: int
    description: str
    completed: bool = False
    priority: Priority = Priority.MEDIUM
    created_at: str = field(default_factory=lambda: datetime.now().isoformat())

    def to_dict(self) -> dict:
        """Convert task to dictionary for JSON serialization."""
        return {
            "id": self.id,
            "description": self.description,
            "completed": self.completed,
            "priority": self.priority.value,
            "created_at": self.created_at,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "Task":
        """Create a Task from a dictionary."""
        return cls(
            id=data["id"],
            description=data["description"],
            completed=data.get("completed", False),
            priority=Priority(data.get("priority", "medium")),
            created_at=data.get("created_at", datetime.now().isoformat()),
        )

task_tracker/storage.py:

"""JSON file storage for tasks."""

import json
from pathlib import Path

from task_tracker.models import Task

DEFAULT_FILE = Path("tasks.json")


def load_tasks(filepath: Path = DEFAULT_FILE) -> list[Task]:
    """Load tasks from the JSON file.

    Returns an empty list if the file doesn't exist.
    """
    if not filepath.exists():
        return []

    with open(filepath, "r") as f:
        data = json.load(f)

    return [Task.from_dict(item) for item in data]


def save_tasks(tasks: list[Task], filepath: Path = DEFAULT_FILE) -> None:
    """Save tasks to the JSON file."""
    data = [task.to_dict() for task in tasks]

    with open(filepath, "w") as f:
        json.dump(data, f, indent=2)


def get_next_id(tasks: list[Task]) -> int:
    """Get the next available task ID."""
    if not tasks:
        return 1
    return max(task.id for task in tasks) + 1

task_tracker/cli.py:

"""CLI entry point and argument parsing."""

import argparse
import sys

from task_tracker.models import Priority, Task
from task_tracker.storage import get_next_id, load_tasks, save_tasks


def add_task(description: str, priority: str = "medium") -> None:
    """Add a new task."""
    if not description.strip():
        print("Error: Task description cannot be empty.")
        sys.exit(1)

    try:
        task_priority = Priority(priority.lower())
    except ValueError:
        print(f"Error: Invalid priority '{priority}'. Use high, medium, or low.")
        sys.exit(1)

    tasks = load_tasks()
    new_task = Task(
        id=get_next_id(tasks),
        description=description.strip(),
        priority=task_priority,
    )
    tasks.append(new_task)
    save_tasks(tasks)
    print(f"Added task #{new_task.id}: {new_task.description} [{task_priority.value}]")


def list_tasks(priority_filter: str | None = None) -> None:
    """List all tasks, optionally filtered by priority."""
    tasks = load_tasks()

    if not tasks:
        print("No tasks found.")
        return

    if priority_filter:
        try:
            filter_priority = Priority(priority_filter.lower())
            tasks = [t for t in tasks if t.priority == filter_priority]
        except ValueError:
            print(
                f"Error: Invalid priority filter '{priority_filter}'. Use high, medium, or low."
            )
            sys.exit(1)

        if not tasks:
            print(f"No tasks with priority '{priority_filter}'.")
            return

    for task in tasks:
        status = "\u2713" if task.completed else "\u25cb"
        priority_label = f"[{task.priority.value}]"
        print(f"  {status} #{task.id} {priority_label} {task.description}")


def complete_task(task_id: int) -> None:
    """Mark a task as completed."""
    tasks = load_tasks()
    task = next((t for t in tasks if t.id == task_id), None)

    if task is None:
        print(f"Error: Task #{task_id} not found.")
        sys.exit(1)

    if task.completed:
        print(f"Task #{task_id} is already completed.")
        return

    task.completed = True
    save_tasks(tasks)
    print(f"Completed task #{task_id}: {task.description}")


def delete_task(task_id: int) -> None:
    """Delete a task."""
    tasks = load_tasks()
    task = next((t for t in tasks if t.id == task_id), None)

    if task is None:
        print(f"Error: Task #{task_id} not found.")
        sys.exit(1)

    tasks.remove(task)
    save_tasks(tasks)
    print(f"Deleted task #{task_id}: {task.description}")


def main() -> None:
    """Main entry point for the CLI."""
    parser = argparse.ArgumentParser(description="Task Tracker CLI")
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # Add command
    add_parser = subparsers.add_parser("add", help="Add a new task")
    add_parser.add_argument("description", help="Task description")
    add_parser.add_argument(
        "--priority",
        "-p",
        choices=["high", "medium", "low"],
        default="medium",
        help="Task priority (default: medium)",
    )

    # List command
    list_parser = subparsers.add_parser("list", help="List all tasks")
    list_parser.add_argument(
        "--priority",
        "-p",
        choices=["high", "medium", "low"],
        help="Filter by priority",
    )

    # Complete command
    complete_parser = subparsers.add_parser("complete", help="Mark a task as complete")
    complete_parser.add_argument("id", type=int, help="Task ID to complete")

    # Delete command
    delete_parser = subparsers.add_parser("delete", help="Delete a task")
    delete_parser.add_argument("id", type=int, help="Task ID to delete")

    args = parser.parse_args()

    if args.command is None:
        parser.print_help()
        sys.exit(0)

    if args.command == "add":
        add_task(args.description, args.priority)
    elif args.command == "list":
        list_tasks(args.priority)
    elif args.command == "complete":
        complete_task(args.id)
    elif args.command == "delete":
        delete_task(args.id)


if __name__ == "__main__":
    main()

task_tracker/__main__.py:

"""Allow running the package with python -m task_tracker."""

from task_tracker.cli import main

main()

.gitignore:

tasks.json
__pycache__/
*.pyc
.pytest_cache/

If Claude's output looks different, that's okay. Claude Code is non-deterministic. The important thing is that the functionality matches: add, list, complete, and delete commands using JSON storage.

Your application code exists. Next up, you'll have Claude write tests and verify that everything actually works.

Verify the Implementation with Tests

Claude Code built your task tracker, but professional developers never trust code until it's been tested. The real power of Claude Code shows up when you give it a verification loop: write tests, run them, see failures, fix the code, and re-run until everything passes.

In this step, you'll tell Claude to write automated tests and iterate until they pass. Then you'll manually test the app yourself to catch anything the tests missed.

In this step, get ready to:
  • Have Claude write and run pytest tests for the task tracker.
  • Manually test the app's add, list, complete, and delete commands.
  • Report any bugs to Claude and have it fix them.
Ask Claude to write and run tests

Automated tests give Claude a concrete way to verify its own work. When you tell Claude to write tests and run them, it enters a self-correction loop. If a test fails, Claude reads the error, adjusts the code, and re-runs until everything passes.

What is a verification loop?

A verification loop is the pattern of running a check (like a test suite), reading the results, fixing any failures, and repeating. This is how professionals use Claude Code. Without a verification step, Claude might produce code that looks right but doesn't actually work.

  • In your Claude Code session, type the following prompt:
Write pytest tests for the task tracker. Test adding a task, listing tasks, completing a task, and deleting a task. Then run the tests and fix any failures.

What does this prompt do?

This single prompt triggers a multi-step workflow. Claude will create a tests/ directory, write test functions covering your core features, run pytest, and fix any failures it finds.

The key phrase is "Then run the tests and fix any failures." This closes the verification loop. Without it, Claude might write tests that don't actually pass.

Watch as Claude creates test files, runs them, and iterates. Claude will ask for permission to create files and execute commands. Allow these actions when prompted.

  • Wait for Claude to finish iterating. You should see output showing all tests passing (green or "passed" indicators).

Claude seems stuck or keeps failing?

If Claude runs into repeated failures, try giving it more specific guidance. For example: "The test for loading from a non-existent file is failing. Make sure load_tasks returns an empty list when the file doesn't exist."

If Claude asks whether to install pytest, allow it. The test runner needs to be installed before tests can run.

Help me troubleshoot Claude Code test failures

Once all tests pass, Claude will show you a summary. Your project now has a tests/ directory with automated test coverage.

✔️ Awesome, I've got everything!

Great. Your tests should be passing and the test files should be in place. Time to try the app yourself.

ⓧ I'd like to double check the full code

Claude's generated tests may differ slightly from this reference, but they should cover the same scenarios. Here's what tests/test_task_tracker.py should look like:

"""Tests for the task tracker application."""

import json
from pathlib import Path

import pytest

from task_tracker.models import Priority, Task
from task_tracker.storage import get_next_id, load_tasks, save_tasks


@pytest.fixture
def tmp_tasks_file(tmp_path: Path) -> Path:
    """Create a temporary tasks file path."""
    return tmp_path / "tasks.json"


@pytest.fixture
def sample_tasks() -> list[Task]:
    """Create sample tasks for testing."""
    return [
        Task(id=1, description="Buy groceries", priority=Priority.HIGH),
        Task(id=2, description="Read a book", priority=Priority.LOW),
        Task(id=3, description="Write tests", completed=True, priority=Priority.MEDIUM),
    ]


class TestStorage:
    """Tests for storage operations."""

    def test_load_tasks_empty_file(self, tmp_tasks_file: Path) -> None:
        """Loading from a non-existent file returns an empty list."""
        tasks = load_tasks(tmp_tasks_file)
        assert tasks == []

    def test_save_and_load_tasks(
        self, tmp_tasks_file: Path, sample_tasks: list[Task]
    ) -> None:
        """Tasks can be saved and loaded back."""
        save_tasks(sample_tasks, tmp_tasks_file)
        loaded = load_tasks(tmp_tasks_file)
        assert len(loaded) == 3
        assert loaded[0].description == "Buy groceries"
        assert loaded[2].completed is True

    def test_get_next_id_empty(self) -> None:
        """Next ID for empty list is 1."""
        assert get_next_id([]) == 1

    def test_get_next_id_with_tasks(self, sample_tasks: list[Task]) -> None:
        """Next ID is one more than the highest existing ID."""
        assert get_next_id(sample_tasks) == 4


class TestTask:
    """Tests for Task model."""

    def test_task_to_dict(self) -> None:
        """Task converts to dictionary correctly."""
        task = Task(id=1, description="Test task", priority=Priority.HIGH)
        data = task.to_dict()
        assert data["id"] == 1
        assert data["description"] == "Test task"
        assert data["priority"] == "high"
        assert data["completed"] is False

    def test_task_from_dict(self) -> None:
        """Task can be created from a dictionary."""
        data = {
            "id": 1,
            "description": "Test task",
            "completed": True,
            "priority": "low",
            "created_at": "2026-01-01T00:00:00",
        }
        task = Task.from_dict(data)
        assert task.id == 1
        assert task.description == "Test task"
        assert task.completed is True
        assert task.priority == Priority.LOW

    def test_task_default_values(self) -> None:
        """Task has sensible defaults."""
        task = Task(id=1, description="Simple task")
        assert task.completed is False
        assert task.priority == Priority.MEDIUM

And the tests/__init__.py file should simply contain:

"""Test package for task tracker."""
Try the app yourself

Automated tests verify individual functions in isolation. Manual testing catches issues that only show up when you use the app the way a real user would.

  • Exit your Claude Code session by typing:
exit

You're now back in PowerShell inside your task-tracker folder.

  • Add a task by running this command:
python -m task_tracker add "Buy groceries"
  • You should see a confirmation like Added task #1: Buy groceries [medium].
  • List your tasks by running:
python -m task_tracker list
  • You should see your task listed with an open circle (○) indicating it's not yet complete.
  • Mark the task as complete by running:
python -m task_tracker complete 1
  • You should see Completed task #1: Buy groceries.
  • Delete the task by running:
python -m task_tracker delete 1
  • You should see Deleted task #1: Buy groceries.

What should I see?

Each command should produce a clear confirmation message. If you run python -m task_tracker list after deleting all tasks, you should see No tasks found.

If any command crashes or produces unexpected output, make a note of what happened. You'll report it to Claude in the next substep.

Seeing ModuleNotFoundError?

Make sure you're running the commands from inside the task-tracker folder. The python -m task_tracker command needs to be at the same level as the task_tracker/ directory.

Help me fix a ModuleNotFoundError with my task tracker

Ask Claude to fix any issues

Describing a real bug to Claude and watching it iterate toward a fix is one of the most important patterns you'll use daily. Even if your manual testing went smoothly, this substep walks you through the bug-reporting workflow.

  • Return to your Claude Code session by running:
claude -c

What does claude -c do?

The -c flag continues your most recent Claude Code conversation. Claude remembers everything from before: the files it created, the tests it wrote, and the project context. You don't need to re-explain anything.

If you found bugs during manual testing, describe them to Claude now. If everything worked, try this edge case to exercise the bug-fix workflow:

  • Type this prompt into your Claude Code session:
When I add a task with no description, the app crashes instead of showing an error. Fix this and add a test for it.

What happens next?

Claude will read the existing code, identify where the empty-description case is handled (or add handling if missing), write a test that exercises that edge case, and run the full test suite to confirm nothing else broke.

This is the complete verification cycle: describe the problem, let Claude fix it, and watch Claude prove the fix works with a test.

  • Wait for Claude to finish iterating. You should see all tests passing, including the new edge case test.

Claude made changes you didn't expect?

If Claude refactors more than you wanted, you can be more specific in your next prompt. For example: "Only change the add_task function. Don't modify anything else." Scoping your prompts tightly gives you more control over what Claude edits.

Help me control what Claude Code edits

Your task tracker is now tested and verified. Next up, you'll learn how to use Claude Code's Git integration to commit your work and manage sessions like a pro.

Use Git Integration and Session Management

You have a fully working task tracker with passing tests. But right now, none of that work is saved in Git.

In this step, you will use Claude Code's built-in Git integration to commit your project, learn essential session management commands for daily use, and add a new feature to experience the complete workflow cycle from a single prompt.

In this step, get ready to:
  • Have Claude commit all your work with a descriptive message.
  • Learn session commands you will use daily.
  • Add a priority feature using the full explore-plan-implement-verify workflow.
Have Claude commit your work

Claude Code integrates directly with Git. It can stage files, write commit messages, and create commits without you ever leaving the session.

  • In your Claude Code session, type the following prompt and press Enter:
Commit all the changes with a descriptive commit message.

Claude will ask for permission to run Git commands. Allow it to proceed.

What happens when Claude commits?

Claude reads through all the files you have, composes a meaningful commit message that describes what was built, runs git add to stage everything, and then runs git commit with that message.

This is the same as committing manually, but Claude writes a professional commit message based on the actual code changes. Review the message it proposes to make sure it accurately describes your task tracker.

  • Review the commit message Claude proposes. It should describe the task tracker project and its features (add, list, complete, delete commands with JSON storage).
  • Allow Claude to proceed with the commit.

Why use Claude for commits?

Claude reads the actual code diff and writes commit messages based on what changed. This produces more descriptive messages than quickly typing one yourself. In professional teams, clear commit messages make it easier to understand project history.

Seeing a Git error?

If Claude reports that Git is not configured, it may need your name and email for commits. Tell Claude: "Configure git with the name 'Developer' and email 'dev@example.com', then commit." Claude will run the config commands and retry.

help me fix a Git configuration error in Claude Code

Learn essential session commands

Claude Code has built-in commands for managing your session. These three commands are the ones you will reach for most often in daily use.

  • Type /help in your Claude Code session and press Enter.
/help

You should see a list of all available slash commands and keyboard shortcuts.

What is /help for?

/help shows every command available in your current session. It is your quick reference when you forget a command name or want to discover new features.

  • Now type /clear and press Enter.
/clear

What does /clear do?

/clear resets Claude's conversation history within the current session. Use this when you are switching to a completely different task and want Claude to start fresh without old context cluttering its responses.

Claude still reads your CLAUDE.md after clearing, so it retains your project conventions. Only the conversation history is wiped.

  • Type exit to leave the Claude Code session.
  • Resume your session by running this command in PowerShell:
claude -c

You should see Claude Code reopen with your previous conversation context restored.

What does claude -c do?

claude -c continues your most recent conversation. This is essential when you close your terminal or step away. Instead of starting from scratch, you pick up right where you left off.

Session not resuming?

Make sure you are still inside the task-tracker folder when running claude -c. If you navigated away, run cd ~/Desktop/task-tracker first, then try again.

help me resume my Claude Code session

Add a feature using the full workflow

Now you will exercise the complete Claude Code workflow in a single prompt. You will ask Claude to add a "priority" field to tasks, with the ability to filter tasks by priority when listing them.

This demonstrates the real power of Claude Code. From one instruction, Claude reads the existing code, plans the changes, implements across multiple files, runs the tests, and commits the result.

  • In your Claude Code session, type the following prompt and press Enter:
Add a "priority" field to tasks (high, medium, low). Tasks should default to medium priority. Add a --priority flag to the add command and a --priority filter to the list command. Update the tests to cover priority. Then run the tests and commit the changes.

What is the full workflow cycle?

This single prompt triggers the entire professional workflow: Claude reads models.py, cli.py, storage.py, and test_task_tracker.py to understand the existing structure.

Then it plans the changes (new Priority enum, updated Task dataclass, new CLI arguments), implements them across all files, runs pytest to verify everything passes, and creates a Git commit describing the priority feature addition.

  • Watch as Claude works through each phase. Allow it to run commands and edit files when prompted.
  • Once Claude finishes, confirm all tests pass by checking its output for a line showing all tests passed with no failures.
  • Verify the priority feature works by exiting Claude Code temporarily. Run this in PowerShell:
python -m task_tracker add "Buy groceries" --priority high
  • Then list your tasks filtered by priority:
python -m task_tracker list --priority high

You should see your task listed with a [high] priority label next to it.

Priority flag not recognized?

If you see an error about unrecognized arguments, Claude may not have finished implementing the feature. Go back into Claude Code with claude -c and tell it: "The --priority flag isn't working on the add command. Fix it and run the tests."

help me debug why the priority feature isn't working

  • Clean up the test task by running:
python -m task_tracker delete 1

What just happened in one prompt?

You just added a feature across four files, with tests, all from a single natural language instruction. This is the pattern you will use daily: describe what you want, let Claude implement it, verify the result, and commit.

✔️ Awesome, I've got everything!

Great. Make sure Claude has committed the priority feature changes. You should now have two commits in your Git history.

ⓧ I'd like to double check the full code

Here is what your key files should look like after Claude adds the priority feature. Your exact code may differ slightly since Claude generates it, but the structure and functionality should match.

"""Task data model."""

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum


class Priority(Enum):
    """Task priority levels."""

    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"


@dataclass
class Task:
    """Represents a single task."""

    id: int
    description: str
    completed: bool = False
    priority: Priority = Priority.MEDIUM
    created_at: str = field(default_factory=lambda: datetime.now().isoformat())

    def to_dict(self) -> dict:
        """Convert task to dictionary for JSON serialization."""
        return {
            "id": self.id,
            "description": self.description,
            "completed": self.completed,
            "priority": self.priority.value,
            "created_at": self.created_at,
        }

    @classmethod
    def from_dict(cls, data: dict) -> "Task":
        """Create a Task from a dictionary."""
        return cls(
            id=data["id"],
            description=data["description"],
            completed=data.get("completed", False),
            priority=Priority(data.get("priority", "medium")),
            created_at=data.get("created_at", datetime.now().isoformat()),
        )
"""CLI entry point and argument parsing."""

import argparse
import sys

from task_tracker.models import Priority, Task
from task_tracker.storage import get_next_id, load_tasks, save_tasks


def add_task(description: str, priority: str = "medium") -> None:
    """Add a new task."""
    if not description.strip():
        print("Error: Task description cannot be empty.")
        sys.exit(1)

    try:
        task_priority = Priority(priority.lower())
    except ValueError:
        print(f"Error: Invalid priority '{priority}'. Use high, medium, or low.")
        sys.exit(1)

    tasks = load_tasks()
    new_task = Task(
        id=get_next_id(tasks),
        description=description.strip(),
        priority=task_priority,
    )
    tasks.append(new_task)
    save_tasks(tasks)
    print(f"Added task #{new_task.id}: {new_task.description} [{task_priority.value}]")


def list_tasks(priority_filter: str | None = None) -> None:
    """List all tasks, optionally filtered by priority."""
    tasks = load_tasks()

    if not tasks:
        print("No tasks found.")
        return

    if priority_filter:
        try:
            filter_priority = Priority(priority_filter.lower())
            tasks = [t for t in tasks if t.priority == filter_priority]
        except ValueError:
            print(
                f"Error: Invalid priority filter '{priority_filter}'. Use high, medium, or low."
            )
            sys.exit(1)

        if not tasks:
            print(f"No tasks with priority '{priority_filter}'.")
            return

    for task in tasks:
        status = "\u2713" if task.completed else "\u25cb"
        priority_label = f"[{task.priority.value}]"
        print(f"  {status} #{task.id} {priority_label} {task.description}")


def complete_task(task_id: int) -> None:
    """Mark a task as completed."""
    tasks = load_tasks()
    task = next((t for t in tasks if t.id == task_id), None)

    if task is None:
        print(f"Error: Task #{task_id} not found.")
        sys.exit(1)

    if task.completed:
        print(f"Task #{task_id} is already completed.")
        return

    task.completed = True
    save_tasks(tasks)
    print(f"Completed task #{task_id}: {task.description}")


def delete_task(task_id: int) -> None:
    """Delete a task."""
    tasks = load_tasks()
    task = next((t for t in tasks if t.id == task_id), None)

    if task is None:
        print(f"Error: Task #{task_id} not found.")
        sys.exit(1)

    tasks.remove(task)
    save_tasks(tasks)
    print(f"Deleted task #{task_id}: {task.description}")


def main() -> None:
    """Main entry point for the CLI."""
    parser = argparse.ArgumentParser(description="Task Tracker CLI")
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # Add command
    add_parser = subparsers.add_parser("add", help="Add a new task")
    add_parser.add_argument("description", help="Task description")
    add_parser.add_argument(
        "--priority",
        "-p",
        choices=["high", "medium", "low"],
        default="medium",
        help="Task priority (default: medium)",
    )

    # List command
    list_parser = subparsers.add_parser("list", help="List all tasks")
    list_parser.add_argument(
        "--priority",
        "-p",
        choices=["high", "medium", "low"],
        help="Filter by priority",
    )

    # Complete command
    complete_parser = subparsers.add_parser("complete", help="Mark a task as complete")
    complete_parser.add_argument("id", type=int, help="Task ID to complete")

    # Delete command
    delete_parser = subparsers.add_parser("delete", help="Delete a task")
    delete_parser.add_argument("id", type=int, help="Task ID to delete")

    args = parser.parse_args()

    if args.command is None:
        parser.print_help()
        sys.exit(0)

    if args.command == "add":
        add_task(args.description, args.priority)
    elif args.command == "list":
        list_tasks(args.priority)
    elif args.command == "complete":
        complete_task(args.id)
    elif args.command == "delete":
        delete_task(args.id)


if __name__ == "__main__":
    main()
"""Tests for the task tracker application."""

import json
from pathlib import Path

import pytest

from task_tracker.models import Priority, Task
from task_tracker.storage import get_next_id, load_tasks, save_tasks


@pytest.fixture
def tmp_tasks_file(tmp_path: Path) -> Path:
    """Create a temporary tasks file path."""
    return tmp_path / "tasks.json"


@pytest.fixture
def sample_tasks() -> list[Task]:
    """Create sample tasks for testing."""
    return [
        Task(id=1, description="Buy groceries", priority=Priority.HIGH),
        Task(id=2, description="Read a book", priority=Priority.LOW),
        Task(id=3, description="Write tests", completed=True, priority=Priority.MEDIUM),
    ]


class TestStorage:
    """Tests for storage operations."""

    def test_load_tasks_empty_file(self, tmp_tasks_file: Path) -> None:
        """Loading from a non-existent file returns an empty list."""
        tasks = load_tasks(tmp_tasks_file)
        assert tasks == []

    def test_save_and_load_tasks(
        self, tmp_tasks_file: Path, sample_tasks: list[Task]
    ) -> None:
        """Tasks can be saved and loaded back."""
        save_tasks(sample_tasks, tmp_tasks_file)
        loaded = load_tasks(tmp_tasks_file)
        assert len(loaded) == 3
        assert loaded[0].description == "Buy groceries"
        assert loaded[2].completed is True

    def test_get_next_id_empty(self) -> None:
        """Next ID for empty list is 1."""
        assert get_next_id([]) == 1

    def test_get_next_id_with_tasks(self, sample_tasks: list[Task]) -> None:
        """Next ID is one more than the highest existing ID."""
        assert get_next_id(sample_tasks) == 4


class TestTask:
    """Tests for Task model."""

    def test_task_to_dict(self) -> None:
        """Task converts to dictionary correctly."""
        task = Task(id=1, description="Test task", priority=Priority.HIGH)
        data = task.to_dict()
        assert data["id"] == 1
        assert data["description"] == "Test task"
        assert data["priority"] == "high"
        assert data["completed"] is False

    def test_task_from_dict(self) -> None:
        """Task can be created from a dictionary."""
        data = {
            "id": 1,
            "description": "Test task",
            "completed": True,
            "priority": "low",
            "created_at": "2026-01-01T00:00:00",
        }
        task = Task.from_dict(data)
        assert task.id == 1
        assert task.description == "Test task"
        assert task.completed is True
        assert task.priority == Priority.LOW

    def test_task_default_values(self) -> None:
        """Task has sensible defaults."""
        task = Task(id=1, description="Simple task")
        assert task.completed is False
        assert task.priority == Priority.MEDIUM

You now know how to commit through Claude Code, manage your sessions, and add entire features with a single prompt. Next up, you will take on the Secret Mission and build a custom slash command that automates your own workflow.

Secret mission

Create a Custom Slash Command

You've used Claude Code's built-in commands like /init and /clear. Now it's time to create your own. In this secret mission, you'll build a custom slash command that automates a code review workflow you can reuse across any project.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing cloud costs or billable resources to worry about. Decide whether to keep your project files and tools installed, pause by closing active sessions, or delete everything entirely.

Resources you used:

  • The task-tracker project folder on your Desktop (local files, Git repository).
  • Claude Code CLI tool (installed natively via PowerShell).
  • Git for Windows (installed for Claude Code's Bash tool).
  • Your Claude Pro, Max, or Console account (subscription-based, not project-specific).

Keep everything running

No action needed. Choose this if you plan to keep building with Claude Code or want to revisit the task tracker later.

  • Your task-tracker folder stays on your Desktop, ready to open with claude any time.
  • Claude Code remains installed and will auto-update in the background.
  • Git for Windows stays available for any future Git or Claude Code usage.
  • Your Claude subscription continues on its billing cycle.

Pause - I'll come back to this later

Close any active Claude Code sessions to free up memory. Your files and tools stay installed for when you return.

  • If you have a Claude Code session open, type exit or press Ctrl+D to close it.
  • Close any PowerShell windows you opened for this project.
  • When you're ready to return, open PowerShell, navigate to ~/Desktop/task-tracker, and run claude -c to continue your most recent conversation.

Delete - I don't want to use this again

Remove all project files and optionally uninstall the tools you installed for this project.

  • Delete the project folder by running this in PowerShell:
Remove-Item -Path "$env:USERPROFILE\Desktop\task-tracker" -Recurse -Force
  • Remove Claude Code's configuration files for this project and your user settings:
Remove-Item -Path "$env:USERPROFILE\.claude" -Recurse -Force
Remove-Item -Path "$env:USERPROFILE\.claude.json" -Force
  • Uninstall the Claude Code binary by running:
Remove-Item -Path "$env:USERPROFILE\.local\bin\claude.exe" -Force
Remove-Item -Path "$env:USERPROFILE\.local\share\claude" -Recurse -Force
  • Uninstall Git for Windows through Windows Settings.
  • Press the Windows key, search for Add or remove programs, and open it.
  • Search for Git in the list, select it, and click Uninstall.
  • Your Claude subscription is managed at claude.ai/settings. Visit that page if you want to change or cancel your plan.

Nice Work!

Nice Work!

You just built a complete Python application without writing a single line of code yourself. Everything was directed through Claude Code, from planning to implementation to testing to committing.

You've learned how to:

  • Built a working Python CLI task tracker entirely by directing Claude Code with natural language prompts.
  • Created a CLAUDE.md file that gives Claude persistent project context, and used the plan-then-execute workflow to architect before coding.
  • Had Claude write automated tests to verify its own work, then used Git integration to create clean, descriptive commits.
  • Created a custom slash command to automate code reviews across your project.

Ready to quiz yourself?