Claude Code Mastery — Direct Track
CC0 — Foundations Module 1 of 16
30 min Beginner
← CC Course Home 🏠 Home CC1: API Essentials →

CC0: Getting Started

Install Claude Code, authenticate, and run your first session in 30 minutes. Build the right mental model from minute one — this isn't a chatbot.

Learning Objectives

By the end of this module you will be able to:

  • Explain what Claude Code is — and what it isn't — in one sentence to a teammate.
  • Install Claude Code with npm and authenticate via Pro/Team/Enterprise account or API key.
  • Open Claude Code inside a real repository and have it read, search, and edit files.
  • Use the eight commands you'll reach for daily: /help, /init, /status, /model, /clear, /compact, /agents, /hooks.
  • Reference files with @, switch into plan mode, and resume yesterday's session with --continue.
Before any installation: 90 seconds on what Claude Code actually is. The wrong mental model leads to frustration in the first hour, and the right one makes everything that follows click.

What is Claude Code?

Everyday Analogy

Think of the difference between a vending machine and a kitchen with a chef.

A vending machine takes a coin and dispenses one snack. You ask a question, you get an answer. ChatGPT-in-a-browser is a vending machine: each turn is isolated, the bot has no real access to your codebase, and what it produces is text you copy-paste.

A kitchen with a chef is a different situation. You hand the chef the menu (your project), they walk to the pantry (read your files), pull ingredients (open dependencies), cook (run tests), taste (check results), adjust seasoning (rerun), and serve. The chef operates in your kitchen; they don't just narrate recipes from across the counter. Claude Code is the kitchen-with-a-chef. It reads your files, runs your tests, calls your APIs, and writes back to disk — all under your supervision.

Technical Definition

Claude Code is a command-line orchestration frameworkA program that runs in your terminal and gives Claude controlled access to tools (read, edit, bash, search) so it can do real work on real codebases. Not a chat client. built on top of the Anthropic Messages API. It runs in your terminal, takes a natural-language request, and executes the work using built-in tools (Read, Write, Edit, Bash, Glob, Grep) plus anything you wire in via MCP servers, skills, subagents, and hooks.

Three properties matter from minute one:

1. It edits your filesystem. Not "here's the code, paste it" — it writes the file. With your permission. With a diff you can review.

2. It runs commands. npm test, git diff, docker build — whatever your terminal can do, Claude Code can do, with your permission.

3. It remembers context. Inside a session, across sessions (with --continue or --resume), and across projects (with CLAUDE.md and Auto Memory). It is not a stateless chat.

The six things Claude Code can do that "AI in your editor" cannot

If you've used Cursor, Copilot, or Gemini Code Assist as inline-completion or chat tools, here's what shifts when you move to Claude Code:

Inline AI vs Claude Code — Capability Surface
Capability Editor AI Claude Code
Multi-file refactorone suggestion at a timereads N files, edits N files, runs tests
Run shell commandsnoyes (with permission)
Read live data via MCPlimitedGitHub, Postgres, Slack, Jira, custom
Custom slash commandsnochecked into repo via .claude/commands/ or .claude/skills/
Hooks & policy enforcementno28 lifecycle events; block / log / route
Headless / CI modenoclaude -p with structured output
Why it matters

The mental shift: stop asking "can the AI write this snippet?" Start asking "can I describe this workflow — read these files, run these commands, validate this output — and let Claude execute it under guardrails I set?" That shift is the difference between using an autocomplete and using a junior teammate.

Now that you know what it is, let's get it running. Three commands.

Install & Authenticate

Prerequisites

  • Node.js 18 or newer — check with node -v. Earlier versions won't run the CLI.
  • A terminal — macOS Terminal/iTerm, Linux any shell, Windows Terminal or WSL2. (PowerShell works; raw cmd.exe is rough.)
  • An Anthropic account — Pro / Team / Enterprise subscription or an API key from console.anthropic.com. Most learners pick the Pro plan ($20/mo) and use OAuth login; pay-as-you-go users prefer the API key.

Step 1 — install the CLI

# Install globally
npm install -g @anthropic-ai/claude-code

# Verify
claude --version
# → @anthropic-ai/claude-code <version>

If you get an EACCES permission error, use a Node version manager (nvm, fnm, volta) instead of sudo. Don't sudo npm install -g — you'll fight permissions every time you update.

Step 2 — authenticate

The first time you run claude, it asks how you want to log in:

$ claude Select login method: > 1. Claude account with subscription (Pro / Max / Team / Enterprise) 2. Anthropic Console account (API usage billing)
Pick the right login method

Claude account with subscription (option 1) — opens a browser, you sign in with your Pro / Max / Team account, OAuth tokens land in ~/.claude.json. Best for individual developers on Pro/Max plans — usage counts against your subscription, not metered API spend.

Anthropic Console / API key (option 2) — signs you into the Console (or you paste an ANTHROPIC_API_KEY via env var). Best for organizations with Anthropic API contracts and budget controls. Usage is metered per token; you'll want cleanupPeriodDays set in settings.json to keep transcripts trimmed.

Bedrock / Vertex (no menu — via env vars) — for shops that already buy Claude through AWS or Google Cloud. Set CLAUDE_CODE_USE_BEDROCK=1 (with AWS_REGION and standard AWS credentials) or CLAUDE_CODE_USE_VERTEX=1 (with CLOUD_ML_REGION and ANTHROPIC_VERTEX_PROJECT_ID) before launching claude, and the CLI routes through your cloud account instead of showing the login menu.

If you ever need to switch login modes, run /logout inside Claude Code and start a new session, or set forceLoginMethod in settings.json at the user level.

Step 3 — (optional) install the IDE extension

Claude Code is CLI-first, but the official VS Code and JetBrains extensions add file-tree integration, side-panel transcript, and selection forwarding. The first time you launch claude from inside a supported IDE's integrated terminal, the CLI offers to install the extension; you can also install it manually from the VS Code Marketplace or JetBrains Marketplace. Run /ide inside Claude Code to connect the current session to your editor.

What just happened?

You installed the CLI and chose how Claude authenticates. From here on, every session draws on your account — no per-session login. The next step is opening Claude Code inside a real codebase, which is where the difference from a browser chat becomes obvious.

The CLI is on your machine. Now point it at a project and watch what happens when "your AI" can actually see the code.

Your First Session

Open a terminal in any project you have writable access to. (If you don't have one handy, git clone one of your old repos — even an old todo-list app works.)

cd ~/my-project
claude

You'll see a welcome banner, the working directory, and a prompt. The session is now live and Claude has visibility into the directory you're in (it does not auto-read every file — it scans on demand using its tools).

Talk to it like a teammate, not like Google

The single biggest mistake new users make is typing queries instead of asks. Compare:

First-Prompt Antipatterns — And the Fix
Don't say Why it's weak Say instead
"How do I add auth?"Generic Q → generic answer. No file access used."Look at this Express app and propose where to add JWT auth. Don't write code yet."
"Fix my tests."Which tests? What's "fix"?"Run npm test. For each failure, identify the root cause and patch the smallest thing that makes it pass."
"What's in this project?"Open-ended → massive context dump."Read package.json and README, then summarize the architecture in 5 bullets."

Try this exact sequence

Inside a real repo, paste these prompts one at a time and watch what Claude does:

> What's in this project? Read package.json and README, then summarize in 5 bullets.

> Run the tests. Tell me which pass, which fail, and the failure messages.

> @src/index.ts — what does this file do? One paragraph.

> In plan mode: how would you add a /healthz endpoint? Don't write code yet.

You will see Claude call tools out loud (Read package.json, Bash: npm test, etc.), pause for permission on the bash command, and stream the results back. That visible tool ribbon is the most important UI in Claude Code. It shows you exactly what was inspected, what was changed, and what was run.

First-time permission prompts

By default Claude Code asks before running Bash, before editing files, and before fetching URLs. You'll see prompts like "Allow this command? [y / always-this-session / always-globally / no]". Pick "always-this-session" for innocuous things like npm test; pick "no" for anything that looks scary. Don't blanket-approve global rules until you understand the permission system — that's CC2.

You've talked to Claude Code in a real repo. Now learn the eight commands you'll reach for daily.

The 5-Minute Tour: Eight Slash Commands That Earn Their Keep

The full slash-command list (and bundled skills) runs into the dozens. These eight pay for themselves on day one:

The Daily-Driver Eight
Command What it does When to reach for it
/helpList every command, including custom onesMemory blank — "what was that command?"
/initGenerate a starting CLAUDE.md by inspecting the repoFirst time in a new project
/statusShow active model, mode, settings sources, MCP servers"Why is it doing X?" — check config first
/modelSwitch model (Sonnet for daily, Opus for hard, Haiku for cheap)A task is too slow or too dumb for current model
/clearWipe the conversation and start fresh in this sessionSwitching tasks — old context will pollute new ask
/compactSummarize older turns to free context windowLong session, hitting token limits
/agentsBrowse / create / edit subagentsSet up a code-reviewer or research subagent (CC4)
/hooksRead-only browser of every active hookDebug "why did that get blocked / not blocked?"

The two non-slash powers: @-references and Plan Mode

Two superpowers don't have slash commands — they're typed directly into prompts:

@ — reference a file or folder by path

Type @ and an autocomplete picker appears. Pick a file (or type the path), and Claude will read it as if you'd handed it the source. Works for folders too — @src/api/ tells Claude "consider this entire directory."

> @src/auth/login.ts — explain the token refresh flow.

> Compare @src/v1/api.ts and @src/v2/api.ts. What's different?

> @docs/architecture.md says we use a queue. Is the code consistent?

Plan Mode — "think first, code later"

Press Shift+Tab twice (cycles through modes) or pass --permission-mode plan at launch. In plan mode Claude reads, searches, and reasons but cannot edit or execute — it produces a written plan you approve before any change happens. Essential for risky tasks like migrations, security work, or "rewrite the auth module."

[plan-mode] $ Plan: Migrate JWT auth from express-jwt to jose Step 1: Install jose, remove express-jwt (package.json) Step 2: Rewrite token verification middleware (src/middleware/auth.ts) Step 3: Update token issuance in login route (src/routes/login.ts) Step 4: Add unit tests for new middleware (tests/middleware/auth.test.ts) Step 5: Run npm test, verify no regressions Files modified: 4 | Risk: MEDIUM | Reversible: Yes (single PR) Shall I proceed? [y]es / [n]o / [e]dit plan

You can edit the plan, ask Claude to expand a step, or say no. Nothing on disk changes until you say yes.

Why it matters

Plan mode is the cheapest way to shift-left bug catches. The bugs that cost you 2 hours to undo? They get caught in the plan readout, before the first character is written. Make a habit: any task that touches more than one file, or anything you'd hesitate to commit blind → plan mode.

Eight commands and two superpowers cover the daily 80%. But what about closing your laptop — does the conversation just disappear?

Sessions Don't Disappear: --continue and --resume

Claude Code persists every session on disk. Today's session ends; tomorrow's continues. There are three patterns:

Three Ways to Continue Yesterday's Conversation
Command What it does When to use
claude --continueReopens the most recent session in this project"Pick up where I left off yesterday"
claude --resumeOpens a picker showing every saved session in this project"Open the conversation about the auth refactor, not the typo fix"
claude --resume <session-id>Reopens a specific session by IDScripting / scheduled jobs / referenced in another tool

Sessions are stored at ~/.claude/projects/<project>/<session-id>.jsonl — one JSON-lines file per session, one directory per project. They're cleaned up after cleanupPeriodDays (default 30).

Pro move: --continue for the most recent, --resume when you have several

The two-keystroke productivity hack:

$ claude --continue   # yesterday's chat reopens, full context intact

> ... continue working ...

# Tomorrow, if you have several saved sessions in this project:
$ claude --resume   # picker shows every saved session by first-prompt summary
What just happened?

You're no longer using Claude Code as a chat-and-forget tool. You're treating sessions like git branches: long-lived, resumable. Your "Tuesday afternoon refactor session" survives Monday's interruptions. This is the workflow most experienced users converge on.

You've seen the moving parts. Time to put them together in a real session.

Hands-On Lab — Boot the PublicRecords API and Map It with Claude

This lab is the foundation for every other module in the track. By the end you will have:

  • A Spring Boot 3 / Java 21 REST API running locally on your machine
  • Eight US UCC (Uniform Commercial Code) lien filings served as JSON
  • Claude Code installed, authenticated, and reading your codebase
  • A real plan for an extension produced by Claude — without writing a single line of code

Time: ~20 minutes if you already have Java and Maven; ~35 if you're installing them today. Difficulty: beginner — every command is explained. Type each prompt — reading isn't the same.

Prerequisites — install and verify

Open a terminal. On macOS: Terminal app. On Linux: any shell. On Windows: PowerShell 7+, NOT the old Command Prompt — heredocs (cat <<'EOF') work in PowerShell 7+ but not the old shell. Install PowerShell 7 with winget install Microsoft.PowerShell if you don't have it.

Run each verification command below. If it fails, follow the install line directly under it.

1. Java JDK 21

$ java -version

You should see:

openjdk version "21.0.4" 2024-07-16
OpenJDK Runtime Environment Temurin-21.0.4+7
OpenJDK 64-Bit Server VM Temurin-21.0.4+7

The first line must start with 21. If it shows 17, 11, or command not found, install JDK 21:

  • macOS: brew install --cask temurin@21
  • Windows: winget install Microsoft.OpenJDK.21 — close and reopen the terminal after install so PATH refreshes.
  • Linux (Ubuntu/Debian): sudo apt update && sudo apt install -y openjdk-21-jdk

If java -version still shows the wrong version after install, your JAVA_HOME environment variable points at the old JDK. Fix:

  • macOS: add export JAVA_HOME=$(/usr/libexec/java_home -v 21) to ~/.zshrc, then source ~/.zshrc.
  • Windows: open Settings → "Edit environment variables for your account" → set JAVA_HOME to C:\Program Files\Microsoft\jdk-21.x.x.x-hotspot. Reopen terminal.

2. Maven 3.9+

$ mvn -v

You should see:

Apache Maven 3.9.6
Maven home: ...
Java version: 21.0.4, vendor: Eclipse Adoptium
Default locale: en_US, platform encoding: UTF-8

Two things to check: Apache Maven 3.9.x or higher, and Java version: 21.x. If Maven reports a different Java version than your shell, fix JAVA_HOME as above. If Maven is missing entirely:

  • macOS: brew install maven
  • Windows: winget install Apache.Maven
  • Linux: sudo apt install -y maven

3. Node.js 20+ (required to install Claude Code)

$ node --version

You should see: v20.x.x or higher (e.g. v22.5.0). If missing or older:

  • macOS: brew install node
  • Windows: winget install OpenJS.NodeJS
  • Linux: follow the package-manager instructions at nodejs.org

4. Git and curl

$ git --version
$ curl --version

Git: any 2.x is fine. curl: any version. Both are preinstalled on macOS and Linux. On Windows 10/11, curl ships with the OS; install Git with winget install Git.Git if missing.

Step 1 — Generate the project skeleton with Spring Initializr

Spring Initializr is a free web form that generates a Maven project with the dependencies you select. It's the standard way Spring teams scaffold new services — faster than hand-writing pom.xml.

1a. Open https://start.spring.io in your browser.

1b. Fill the form fields exactly as shown — match the spelling and case:

FieldValue
ProjectMaven
LanguageJava
Spring Boot3.3.4 (or the latest 3.3.x release)
Groupcom.publicrecords
Artifactpublicrecords-api
Namepublicrecords-api
DescriptionUCC public records REST API
Package namecom.publicrecords.api
PackagingJar
Java21

1c. Click the ADD DEPENDENCIES button on the right side. In the search box, find and add each of these one at a time:

  • Spring Web — gives REST controllers + an embedded Tomcat web server
  • Spring Data JPA — database access layer (Hibernate under the hood)
  • H2 Database — in-memory database for development and tests
  • Validation — Bean Validation (@Valid, @NotBlank, etc.)

You should now see four chips listed under the Dependencies heading on the right side.

1d. Click the green GENERATE button at the bottom. A file publicrecords-api.zip downloads to your default downloads folder.

1e. Move the zip to a clean working directory and extract it. macOS / Linux:

$ mkdir -p ~/cc-labs
$ cd ~/cc-labs
$ mv ~/Downloads/publicrecords-api.zip .
$ unzip publicrecords-api.zip
$ cd publicrecords-api
$ ls

Windows PowerShell:

PS> mkdir $HOME\cc-labs
PS> cd $HOME\cc-labs
PS> Move-Item $HOME\Downloads\publicrecords-api.zip .
PS> Expand-Archive publicrecords-api.zip
PS> cd publicrecords-api\publicrecords-api
PS> ls

You should see:

HELP.md   mvnw   mvnw.cmd   pom.xml   src/

Note your project root directory. Every command in this lab assumes you're in this folder. From here on we'll abbreviate it as the project root.

Step 2 — Confirm the empty skeleton compiles

Before we add code, prove the skeleton is wired correctly. From the project root:

$ mvn compile

Maven downloads the Spring Boot dependencies on first run (~30 seconds on a fast connection, several minutes on slow). When done you'll see:

[INFO] BUILD SUCCESS
[INFO] Total time:  18.234 s
[INFO] Finished at: 2026-05-05T14:08:42-05:00

If you see Source option 21 is no longer supported, your Maven is using a JDK older than 21. Run mvn -v, check the Java version line, and fix JAVA_HOME as in the Prerequisites section.

Step 3 — See what Initializr generated

From the project root:

$ tree src       # macOS/Linux. Windows: Get-ChildItem -Recurse src

Yours will look approximately like this (the placeholder class name varies by Initializr version):

src
├── main
│   ├── java
│   │   └── com
│   │       └── publicrecords
│   │           └── api
│   │               └── PublicrecordsApiApplication.java
│   └── resources
│       ├── application.properties
│       ├── static
│       └── templates
└── test
    └── java
        └── com
            └── publicrecords
                └── api
                    └── PublicrecordsApiApplicationTests.java

Initializr only gives you the @SpringBootApplication bootstrap class and a placeholder test — nothing domain-specific. Steps 4–12 add the rest. Heads up: Initializr names the class PublicrecordsApiApplication (lowercase 'r' in "records"), which doesn't match the standard Java convention. We'll rename it next.

Step 4 — Rename the application class to use proper CamelCase

Initializr lowercased "Records" because of how it derives class names. Standard Java convention is PublicRecordsApiApplication (capital R). Fix the filename first:

mv src/main/java/com/publicrecords/api/PublicrecordsApiApplication.java src/main/java/com/publicrecords/api/PublicRecordsApiApplication.java
mv src/test/java/com/publicrecords/api/PublicrecordsApiApplicationTests.java src/test/java/com/publicrecords/api/PublicRecordsApiApplicationTests.java

Windows PowerShell uses Move-Item with the same paths.

Now replace the contents of the renamed main class with the proper class name:

Java Spring Boot Bootstrap
src/main/java/com/publicrecords/api/PublicRecordsApiApplication.java
package com.publicrecords.api;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class PublicRecordsApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(PublicRecordsApiApplication.class, args);
    }
}

And replace the test class to match:

Java Context Smoke Test
src/test/java/com/publicrecords/api/PublicRecordsApiApplicationTests.java
package com.publicrecords.api;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class PublicRecordsApiApplicationTests {

    @Test
    void contextLoads() {}
}
How it works

@SpringBootApplication is a meta-annotation that bundles three things: @Configuration (this class declares Spring beans), @EnableAutoConfiguration (Spring Boot auto-configures based on what's on the classpath — H2, JPA, Web), and @ComponentScan (scan this package and below for @Component / @Service / @RestController classes). One annotation; entire framework wired up.

Run mvn compile to confirm the rename worked. BUILD SUCCESS = good.

What Just Happened

Standard CamelCase class names + a one-line test that proves the Spring context can boot. The empty contextLoads test is more useful than it looks — if your bean wiring is broken, this test fails fast, even before any other tests run.

Step 5 — Create the Filing entity

The Filing class is a Java object that represents one row in the database. We're going to add it now — before this step the project has no domain code, just the Spring Boot bootstrap.

How it works

JPA (Jakarta Persistence API) is the Java standard for object-relational mapping — turning database rows into Java objects and back. Hibernate is the implementation Spring Boot uses by default.

  • @Entity tells Hibernate "this Java class corresponds to a database row."
  • @Table(name = "filings") sets the actual table name (otherwise it'd default to filing).
  • @Id + @GeneratedValue(IDENTITY)id is the primary key, auto-incremented by the DB.
  • @Column(name = "debtor_name") lets us use camelCase in Java but keep snake_case in SQL — the convention in both worlds.
  • @NotBlank + @Size are Bean Validation annotations — checked when the object is bound from an HTTP request body. Invalid input returns 400 before your code runs.

First make the package directory:

mkdir -p src/main/java/com/publicrecords/api/filing

Now create the file at the path shown in the header below, and paste the entire contents:

Java JPA Entity
src/main/java/com/publicrecords/api/filing/Filing.java
package com.publicrecords.api.filing;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.time.Instant;

@Entity
@Table(name = "filings")
public class Filing {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Size(min = 2, max = 2)
    @Column(nullable = false, length = 2)
    private String state;

    @NotBlank
    @Column(name = "debtor_name", nullable = false)
    private String debtorName;

    @NotBlank
    @Column(name = "secured_party", nullable = false)
    private String securedParty;

    @Column(name = "collateral_description", length = 1000)
    private String collateralDescription;

    @Column(name = "filed_at", nullable = false)
    private Instant filedAt;

    @Column(name = "expires_at")
    private Instant expiresAt;

    public Filing() {}

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getState() { return state; }
    public void setState(String state) { this.state = state; }
    public String getDebtorName() { return debtorName; }
    public void setDebtorName(String debtorName) { this.debtorName = debtorName; }
    public String getSecuredParty() { return securedParty; }
    public void setSecuredParty(String securedParty) { this.securedParty = securedParty; }
    public String getCollateralDescription() { return collateralDescription; }
    public void setCollateralDescription(String collateralDescription) { this.collateralDescription = collateralDescription; }
    public Instant getFiledAt() { return filedAt; }
    public void setFiledAt(Instant filedAt) { this.filedAt = filedAt; }
    public Instant getExpiresAt() { return expiresAt; }
    public void setExpiresAt(Instant expiresAt) { this.expiresAt = expiresAt; }
}
What Just Happened

You created the JPA entity that defines the shape of a UCC filing in your database. When the app boots, Hibernate will use this class to auto-create a filings table with seven columns matching your fields. The validation annotations will reject bad input at the HTTP boundary before it reaches your business logic.

Step 6 — Create the FilingRepository

How it works

Spring Data JPA generates the implementation of this interface for you at runtime. You only declare what queries you need; Spring figures out the SQL.

  • JpaRepository<Filing, Long> — says "this is a repository for Filing entities, with Long primary keys." You inherit findAll(), findById(), save(), delete(), count(), and pagination methods for free.
  • findByState(String state) — the method name follows Spring Data's derived query naming: "find by <property>." Spring translates it to SELECT * FROM filings WHERE state = ?. No implementation needed.
Java Spring Data Repository
src/main/java/com/publicrecords/api/filing/FilingRepository.java
package com.publicrecords.api.filing;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FilingRepository extends JpaRepository<Filing, Long> {
    List<Filing> findByState(String state);
}
What Just Happened

You declared a database access layer in 6 lines of code. At runtime Spring will scan for JpaRepository sub-interfaces and generate proxy classes that implement them. Your FilingController in the next step will inject this repository as a dependency — no new FilingRepository() needed.

Step 7 — Create the FilingController

The controller is the HTTP layer — the actual REST endpoints clients call.

How it works
  • @RestController = @Controller + @ResponseBody. It tells Spring to serialize every method's return value to JSON automatically.
  • @RequestMapping("/filings") sets the URL prefix; every method below is rooted under /filings.
  • Constructor injection — the FilingRepository is a constructor parameter. Spring sees this and passes the repository proxy in at startup. No @Autowired needed; one constructor is enough.
  • @PathVariable binds {id} in the URL to the method parameter.
  • ResponseEntity lets us return a proper 404 when the filing isn't found, instead of throwing.
Java REST Controller
src/main/java/com/publicrecords/api/filing/FilingController.java
package com.publicrecords.api.filing;

import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/filings")
public class FilingController {

    private final FilingRepository repository;

    public FilingController(FilingRepository repository) {
        this.repository = repository;
    }

    @GetMapping
    public List<Filing> list(@RequestParam(required = false) String state) {
        return state == null ? repository.findAll() : repository.findByState(state);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Filing> findById(@PathVariable Long id) {
        return repository.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
}
What Just Happened

You wired up two REST endpoints: GET /filings (with optional ?state=XX filter) and GET /filings/{id}. The controller doesn't talk to the database directly — it asks the repository, which Spring auto-implements. This separation lets us swap H2 for Postgres in CC6 without touching the controller.

Step 8 — Create the Pii utility class

A small helper that masks sensitive identifiers (SSN, EIN, account numbers) before they reach logs. CC1's CLAUDE.md will codify a rule that references this class.

How it works

For US public-records data, leaking an SSN to logs can be a regulatory incident. The fix is mechanical: every time you log a sensitive identifier, mask it. This helper takes the last 4 chars and prefixes ***, so 123-45-6789 becomes ***6789. final class + private constructor = nobody can instantiate or extend it; it's a pure utility.

First make the package directory:

mkdir -p src/main/java/com/publicrecords/api/common
Java PII Helper
src/main/java/com/publicrecords/api/common/Pii.java
package com.publicrecords.api.common;

public final class Pii {

    private Pii() {}

    public static String mask(String value) {
        if (value == null || value.length() < 4) {
            return "***";
        }
        return "***" + value.substring(value.length() - 4);
    }
}
What Just Happened

You added a reusable masking helper. Any code that logs a sensitive value can call Pii.mask(ssn) instead of logging it raw. CC1 will write the rule into CLAUDE.md so Claude refuses to log raw PII; CC4 builds an audit subagent that scans for violations.

Step 9 — Replace application.properties with application.yml

Initializr generated application.properties. We'll use YAML instead because it's nicer for nested configuration (which we'll have plenty of by CC6's MCP setup).

First delete the properties file:

rm src/main/resources/application.properties

Windows PowerShell: Remove-Item src\main\resources\application.properties

YAML Spring Config
src/main/resources/application.yml
server:
  port: 8080

spring:
  datasource:
    url: jdbc:h2:mem:ucc;DB_CLOSE_DELAY=-1
    username: sa
    password: ""
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: false
  h2:
    console:
      enabled: true
      path: /h2
  sql:
    init:
      mode: always

logging:
  level:
    com.publicrecords: INFO

What each setting does:

  • jdbc:h2:mem:ucc — in-memory database named "ucc" that lives only inside the JVM (no disk file).
  • ddl-auto: create-drop — JPA creates the tables from your @Entity classes on startup, drops them on shutdown.
  • sql.init.mode: always — runs data.sql after table creation on every boot. Without this, your seed data won't load.
  • h2.console.enabled: true — exposes a web-based H2 admin UI at /h2.
What Just Happened

Spring Boot's "convention over configuration" means most defaults are fine; you only override what's specific to your project. This application.yml tells Spring: use H2 in memory, recreate the schema on every boot, and run our seed script.

Step 10 — Create data.sql with eight UCC filings

Spring Boot automatically runs any data.sql on the classpath after the schema is created (because we set sql.init.mode: always). This is your seed data — eight realistic but fake UCC filings across six US states.

SQL Seed Data
src/main/resources/data.sql
INSERT INTO filings (state, debtor_name, secured_party, collateral_description, filed_at, expires_at) VALUES
  ('TX', 'Lone Star Holdings LLC', 'First National Bank', 'All inventory and accounts receivable', '2023-04-12T00:00:00Z', '2028-04-12T00:00:00Z'),
  ('TX', 'Pecos River Logistics Inc.', 'Wells Fargo Equipment Finance', '2022 Peterbilt 579 tractor units (VINs in Schedule A)', '2024-01-08T00:00:00Z', '2029-01-08T00:00:00Z'),
  ('CA', 'Pacific Coast Wineries', 'Bank of the West', 'Specific equipment per Schedule A', '2023-09-21T00:00:00Z', '2028-09-21T00:00:00Z'),
  ('NY', 'Hudson Valley Foods Corp', 'Citibank N.A.', 'Inventory of perishable goods and proceeds', '2024-06-03T00:00:00Z', '2029-06-03T00:00:00Z'),
  ('FL', 'Coral Gables Partners LP', 'Truist Bank', 'Furniture, fixtures, and equipment', '2022-11-15T00:00:00Z', '2027-11-15T00:00:00Z'),
  ('IL', 'Chicago Steel Works', 'JPMorgan Chase', 'All assets per security agreement dated 2024-02-01', '2024-02-01T00:00:00Z', '2029-02-01T00:00:00Z'),
  ('OH', 'Buckeye Manufacturing', 'KeyBank', 'Specific equipment listed in Exhibit A', '2023-07-19T00:00:00Z', '2028-07-19T00:00:00Z'),
  ('PA', 'Liberty Bell Foundries', 'PNC Bank', 'Accounts receivable and proceeds', '2024-03-10T00:00:00Z', '2029-03-10T00:00:00Z');
What Just Happened

Eight rows of fake UCC data — safe to commit. The columns match the JPA entity's @Column names exactly (snake_case in SQL, camelCase in Java). When the app boots, Hibernate creates the table, then Spring runs this script and loads the rows.

Step 11 — Add the controller test

How it works
  • @SpringBootTest spins up the full application context for the test — database, controllers, repository, the works. Slow but thorough.
  • @AutoConfigureMockMvc gives us a MockMvc bean we can inject and use to fire HTTP requests without actually starting a network server.
  • jsonPath("$.length()") asserts on the structure of the JSON response — here, that the response array has 8 entries.

First create the test package directory:

mkdir -p src/test/java/com/publicrecords/api/filing
Java MockMvc Test
src/test/java/com/publicrecords/api/filing/FilingControllerTest.java
package com.publicrecords.api.filing;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
class FilingControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    void listsFilingsFromSeedData() throws Exception {
        mvc.perform(get("/filings"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(8));
    }

    @Test
    void filtersByState() throws Exception {
        mvc.perform(get("/filings?state=TX"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.length()").value(2));
    }

    @Test
    void returnsNotFoundForUnknownId() throws Exception {
        mvc.perform(get("/filings/9999"))
                .andExpect(status().isNotFound());
    }
}
What Just Happened

Three smoke tests that exercise every endpoint: list-all (8 rows from seed data), filter by state (2 Texas rows), and 404-on-unknown-id. These tests will run on every mvn test — CC7 wires them into a GitHub Action so every PR gets verified.

Step 12 — Run the tests to confirm everything works

$ mvn test

You should see (last few lines):

[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] BUILD SUCCESS

If a test fails, the error usually points at one of three things: a typo in your file paths (filing/ not filings/), a missing import, or a missing data.sql. Re-check Steps 5–11 against your files; the contents must match exactly.

Step 13 — Boot the API

$ mvn spring-boot:run

You'll see the Spring Boot banner, then startup lines ending in:

2026-05-05T14:12:01  INFO ... Starting PublicRecordsApiApplication ...
2026-05-05T14:12:03  INFO ... Tomcat started on port 8080 (http)
2026-05-05T14:12:03  INFO ... Started PublicRecordsApiApplication in 2.847 seconds

The server is running. Leave this terminal open. Don't press Ctrl+C — that stops the server.

Boot failures — the three you'll most likely hit

"Web server failed to start. Port 8080 was already in use." — another process owns 8080. Find and kill it: macOS/Linux lsof -i :8080 then kill <PID>. Windows PowerShell Get-NetTCPConnection -LocalPort 8080 then Stop-Process -Id <PID>. Or change the port: edit application.yml and set server.port: 8081, then rerun. Use 8081 in every URL below.

"Source option 21 is no longer supported." — Maven is using a JDK older than 21. Run mvn -v and check the Java version line. Fix JAVA_HOME as in Prerequisites.

"Failed to determine a suitable driver class" — H2 not on the classpath. Re-check that your pom.xml has com.h2database:h2 as a dependency.

Step 14 — Hit each endpoint from a second terminal

Open a second terminal, leaving the server running in the first. Navigate to the project root:

$ cd ~/cc-labs/publicrecords-api

14a. List all filings

$ curl http://localhost:8080/filings

You should see: a JSON array of 8 filings. The first entry looks like:

[{"id":1,"state":"TX","debtorName":"Lone Star Holdings LLC","securedParty":"First National Bank","collateralDescription":"All inventory and accounts receivable","filedAt":"2023-04-12T00:00:00Z","expiresAt":"2028-04-12T00:00:00Z"}, ...]

If you see [] (empty array), the seed data didn't load — confirm spring.sql.init.mode: always is in application.yml exactly as shown in Step 9, and that data.sql from Step 10 exists with INSERT statements. Stop the server (Ctrl+C in the first terminal), fix, rerun.

14b. Get one filing by ID

$ curl http://localhost:8080/filings/1

You should see: a single JSON object (no surrounding []) for the first Texas filing.

14c. Filter by state

$ curl "http://localhost:8080/filings?state=TX"

You should see: 2 Texas filings (Lone Star Holdings + Pecos River Logistics). The URL is in quotes because ? is special to many shells when not quoted.

14d. (Optional) Browse the H2 console

Open http://localhost:8080/h2 in your browser. Fill in:

  • JDBC URL: jdbc:h2:mem:ucc
  • User Name: sa
  • Password: leave blank

Click Connect, type SELECT * FROM FILINGS; in the SQL pane, click Run. You'll see 8 rows.

Step 15 — Initialize the project as a git repo

Subsequent modules (CC1+) will commit incrementally, so set up version control now. From the project root in your second terminal:

$ git init
$ git branch -M main

Create .gitignore at the project root:

target/
*.class
.idea/
.vscode/
*.iml
.DS_Store
.claude/local/
application-local.yml
application-prod.yml

Then make the initial commit:

$ git add .
$ git commit -m "feat: initial PublicRecords API with Filing CRUD"

Run git status — you should see nothing to commit, working tree clean. The project is now version-controlled.

Step 16 — Install Claude Code

$ npm install -g @anthropic-ai/claude-code
$ claude --version

You should see: 2.x.x (e.g. 2.1.59).

npm install fails with EACCES on macOS/Linux

Two fixes. Either retry with sudo npm install -g @anthropic-ai/claude-code — quick but uses root. Or set up a user-local npm prefix (cleaner long-term):

$ npm config set prefix ~/.npm-global
$ echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.zshrc
$ source ~/.zshrc
$ npm install -g @anthropic-ai/claude-code

Step 17 — Authenticate and launch Claude Code

From the project root (second terminal):

$ claude

On first launch, Claude Code prompts you to log in. Pick the option that matches your access (Anthropic Console / Claude.ai subscription / API key) and follow the browser-based flow. After auth, you'll see the interactive UI:

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Claude Code  v2.x.x
 Project: publicrecords-api  (~/cc-labs/publicrecords-api)
 Model: claude-opus-4-7  Mode: default
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

>  

The Project line MUST match your project root. If it doesn't, exit (Ctrl+D), cd to the right place, rerun claude. Everything below assumes Claude Code is anchored on the project root.

Step 18 — Map the codebase by asking Claude

Type each prompt at the > cursor and press Enter.

Prompt 1 — five-bullet tour

Read pom.xml and the src/main/java tree, then give me a 5-bullet
tour of this codebase. Identify the entity, repository, and controller.

Watch the tool ribbon — Claude makes Read(pom.xml), Glob(src/main/java/**/*.java), Read(...) calls visibly. After ~5–10 seconds you should get a response like:

- Spring Boot 3.3 / Java 21 REST service named publicrecords-api
- Single domain: UCC lien filings, package com.publicrecords.api.filing
- Filing.java is the JPA entity (id, state, debtorName, securedParty,
  collateralDescription, filedAt, expiresAt) mapped to table "filings"
- FilingRepository extends JpaRepository<Filing, Long> with one
  derived query: findByState(String)
- FilingController exposes GET /filings (with optional ?state filter)
  and GET /filings/{id} returning ResponseEntity for 404 handling

Prompt 2 — identify build and test commands

What's the build command? What's the test command?
Don't run them — just identify from pom.xml.

Expected answer: build = mvn package (or mvn compile); test = mvn test. The "don't run them" part teaches Claude to differentiate identifying from executing.

Prompt 3 — analyze a specific file via @-reference

Type @ at the prompt — a file picker appears. Either arrow-down to navigate or paste the path:

@src/main/java/com/publicrecords/api/filing/FilingController.java
Explain in one paragraph what this controller does and identify
one missing concern (think: validation, error handling, security).

The @-reference loads the file inline, so Claude sees it without a Read call. A good response flags one of: missing @Valid on inputs, no rate limiting, no audit logging, no pagination, raw id exposed in URLs (tenancy concern). This kind of analysis is what CC1's CLAUDE.md will codify into project rules.

Step 19 — Plan a real change in plan mode (don't execute)

Plan mode is read-only — Claude can read but cannot edit or run shell commands. Press Shift+Tab until the bottom-right of the prompt shows plan.

In plan mode: how would you add a new endpoint
GET /filings/expiring?days=N that returns filings whose
expires_at falls within the next N days? Identify the file(s)
to edit, what gets added, what test goes where. Don't write code yet.

Claude reads more of the codebase and outputs a structured plan. Expected shape:

PLAN — add GET /filings/expiring?days=N

1. FilingRepository.java
   Add: List<Filing> findByExpiresAtBetween(Instant from, Instant to);

2. FilingController.java
   Add @GetMapping("/expiring") method:
     - @RequestParam(defaultValue="90") @Min(1) @Max(365) int days
     - Compute from = Instant.now()
     - Compute to = from.plus(days, ChronoUnit.DAYS)
     - Return repository.findByExpiresAtBetween(from, to)
   Add @Validated to controller class so @Min/@Max trigger.

3. FilingControllerTest.java (extend existing test class)
   - GET /filings/expiring?days=1825 returns the right subset
   - GET /filings/expiring (no param) defaults to 90 days
   - GET /filings/expiring?days=999 returns 400 with validation error

Files changed: 3. New tests: 3. Estimated implementation: ~15 lines.

Read the plan critically. Did Claude miss anything? Reply with corrections — the plan iterates. Things you might push back on: time-zone handling, response envelope vs raw array, behavior when N is omitted vs zero. Push back; the plan should improve.

Step 20 — Exit cleanly without keeping changes

Plan mode means nothing was written, so there's nothing to revert. Exit Claude Code by pressing Ctrl+D at the prompt (or Ctrl+C twice). Then verify the working tree is clean:

$ git status

You should see:

On branch main
nothing to commit, working tree clean

Then stop the server — press Ctrl+C in the first terminal. You'll see Stopped PublicRecordsApiApplication.

Troubleshooting reference

SymptomLikely causeFix
Spring Boot fails: "Port 8080 already in use"Another process owns 8080Kill it (lsof / Get-NetTCPConnection) or set server.port: 8081 in application.yml
java -version shows 17 or 11 after installJAVA_HOME points at the old JDKSet JAVA_HOME to JDK 21 path; restart shell
curl /filings returns []data.sql didn't loadConfirm spring.sql.init.mode: always in application.yml; confirm data.sql exists with INSERT lines
curl returns "Connection refused"Server not running, wrong port, or still bootingLook for "Started PublicRecordsApiApplication" in server terminal
mvn test fails with "package com.publicrecords... does not exist"File in wrong package directoryPath must be src/main/java/com/publicrecords/api/<package>/ — check for typos
claude command not foundnpm global bin not in PATHRun echo $PATH; add $(npm config get prefix)/bin if missing
Claude Code shows wrong project pathLaunched from wrong directoryExit, cd to project root, relaunch
Plan mode let Claude write a fileMode toggled off mid-sessionCheck the bottom-right indicator; Shift+Tab to re-enter plan
cat << EOF heredoc fails on WindowsUsing Command Prompt instead of PowerShell 7+winget install Microsoft.PowerShell, then run pwsh
Lab complete — what you built

A real Spring Boot 3 / Java 21 REST API you built file by file — entity, repository, controller, helper class, YAML config, seed data, MockMvc tests — running on port 8080 and serving 8 UCC filings as JSON. Claude Code installed, authenticated, anchored on your project, successfully producing a 5-bullet codebase tour, an analysis of FilingController.java, and a structured plan for a new endpoint — all without writing a single line. Working tree clean and committed to git.

The next 14 modules build on this project: CC1–CC2 cover API foundations and prompt engineering, CC3 documents the UCC domain via CLAUDE.md, CC4 locks down dangerous Maven and SQL commands, CC5 generates new resources via slash commands, CC6 delegates to subagents, CC7 enforces formatting via hooks, CC8 takes a deep dive on tool use, CC9 connects MCP servers (consumer + custom), CC10 covers caching/citations/thinking, CC11 builds eval gates, CC12 wires RAG, CC13 covers workflow patterns and computer use, CC14 ships everything through GitHub Actions.

Where to Go Next

The course is linear. Each module assumes the previous. Skipping ahead works if you only need one topic, but the cross-references make CC1→CC14 the right path.

The 14 Modules That Follow
Next Module You'll be able to…
CC1API EssentialsModels, Messages API, system prompts, streaming, structured output
CC2Prompt EngineeringClarity, specificity, XML tags, few-shot, thinking triggers
CC3CLAUDE.md & MemoryConfigure Claude per repo + Auto Memory
CC4Permissions & SandboxPick the right safety mode + lock it down at OS level
CC5Skills & Slash CommandsBuild your own /deploy, /triage, /review
CC6SubagentsDelegate research / review to specialists w/ their own tools
CC7HooksAuto-format, block dangerous commands, route via HTTP
CC8Tool Use Deep DiveThe tool-use loop, schemas, fine-grained, built-in tools
CC9MCP — Consumer & ServerWire in GitHub/Postgres + build your own MCP server
CC10Claude FeaturesThinking, prompt caching, citations, image/PDF, code-exec
CC11EvaluationTest datasets, code & LLM-as-judge graders, CI gates
CC12RAGChunking, embeddings, BM25, hybrid search as MCP/subagent
CC13Workflows & Computer UseChaining/parallel/routing, agent loops, GUI automation
CC14Power User & CI/CDTwo-Claude review, worktrees, GitHub Actions, headless
Quick check before you move on.

Knowledge Check

Five questions. Answer each, then click to reveal.

1. Which of the following is the strongest mental model for Claude Code?

A
An autocomplete plugin that lives inside your editor.
B
A web chat with copy-paste code suggestions.
C
A CLI that takes natural-language tasks and orchestrates real tools (read, edit, bash, MCP) under your supervision.
D
A read-only documentation assistant.
Correct. Claude Code is a tool-using orchestration framework. The "vending machine vs kitchen with a chef" analogy: it operates in your kitchen rather than narrating recipes from afar.
Not quite. Claude Code reads and modifies your filesystem, runs commands, and remembers context across sessions — that's option C.

2. You're new to a 100k-line repository and want Claude Code to suggest where to add JWT auth. What's the right first prompt?

A
"How do I add JWT auth?"
B
"Just write the auth code."
C
"In plan mode: read the codebase and propose where JWT auth would fit. Don't write code yet."
D
"Show me an example JWT auth implementation."
Correct. Plan mode lets Claude read & reason without editing. For risky or unfamiliar work, plan first — it's the cheapest way to catch bugs before they exist.
Look again. "How do I…" is generic Q&A; it doesn't use Claude Code's file access. Plan mode (option C) lets Claude read the code and reason without editing.

3. You're starting a long session in a new repo. Which command should you run first?

A
/clear
B
/compact
C
/init — generates a starter CLAUDE.md
D
/hooks
Correct. /init inspects the repo and produces a CLAUDE.md scaffold — the starting point for project-specific config (covered in CC1).
Not quite. /clear wipes context (no current context to wipe), /compact compresses old turns (none yet), /hooks shows hooks (which you haven't configured). /init is the right first move.

4. You closed your terminal yesterday mid-task. Today you want exactly that conversation back, not a different one. What do you run?

A
claude --continue
B
claude --resume — pick the named session from the list
C
claude --new
D
claude /restore
Correct. --continue reopens the most recent session, but if you ran multiple yesterday, you want --resume to pick the specific one from the saved-session list.
Look again. --continue picks the most recent (might be a different task). --resume opens a picker so you can choose the right session.

5. You install Claude Code and the first prompt asks how to authenticate. You're on a Pro subscription, want OAuth login (no API key tracking). Which option?

A
Claude account with subscription — opens a browser, signs in with Pro account.
B
Anthropic Console / API-key billing.
C
AWS Bedrock provider.
D
Vertex AI provider.
Correct. "Claude account with subscription" is the OAuth path — usage counts against your Pro subscription, no separate API budget. The Console option is for metered API billing; Bedrock/Vertex are for orgs that buy through a cloud (set via env vars, not the menu).
Not quite. Pro subscribers want option A — OAuth-based, usage covered by the subscription. API key / Console billing is metered API spending separately from any subscription.

Module Summary

You now have:

  • The right mental model: Claude Code is a tool-using orchestration framework, not a chatbot.
  • A working install with the auth method that matches your account (OAuth / API key / Bedrock-Vertex).
  • The four ways to talk to it: direct prompts, @-references, plan mode (Shift+Tab), bash-permission flow.
  • The eight commands you'll reach for daily: /help, /init, /status, /model, /clear, /compact, /agents, /hooks.
  • Session persistence: --continue (most recent) and --resume (picker over every saved session in the project).

In CC1 you'll meet the Messages API directly — the foundation under everything Claude Code does. Then CC2 covers prompt engineering, and CC3 returns to Claude Code itself with the 5-tier CLAUDE.md cascade and Auto Memory.