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
npmand 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.
What is Claude Code?
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.
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:
| Capability | Editor AI | Claude Code |
|---|---|---|
| Multi-file refactor | one suggestion at a time | reads N files, edits N files, runs tests |
| Run shell commands | no | yes (with permission) |
| Read live data via MCP | limited | GitHub, Postgres, Slack, Jira, custom |
| Custom slash commands | no | checked into repo via .claude/commands/ or .claude/skills/ |
| Hooks & policy enforcement | no | 28 lifecycle events; block / log / route |
| Headless / CI mode | no | claude -p with structured output |
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.
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.exeis 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 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.
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.
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:
| 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.
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.
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:
| Command | What it does | When to reach for it |
|---|---|---|
/help | List every command, including custom ones | Memory blank — "what was that command?" |
/init | Generate a starting CLAUDE.md by inspecting the repo | First time in a new project |
/status | Show active model, mode, settings sources, MCP servers | "Why is it doing X?" — check config first |
/model | Switch model (Sonnet for daily, Opus for hard, Haiku for cheap) | A task is too slow or too dumb for current model |
/clear | Wipe the conversation and start fresh in this session | Switching tasks — old context will pollute new ask |
/compact | Summarize older turns to free context window | Long session, hitting token limits |
/agents | Browse / create / edit subagents | Set up a code-reviewer or research subagent (CC4) |
/hooks | Read-only browser of every active hook | Debug "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."
You can edit the plan, ask Claude to expand a step, or say no. Nothing on disk changes until you say yes.
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.
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:
| Command | What it does | When to use |
|---|---|---|
claude --continue | Reopens the most recent session in this project | "Pick up where I left off yesterday" |
claude --resume | Opens 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 ID | Scripting / 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
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.
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, thensource ~/.zshrc. - Windows: open Settings → "Edit environment variables for your account" → set
JAVA_HOMEtoC:\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:
| Field | Value |
|---|---|
| Project | Maven |
| Language | Java |
| Spring Boot | 3.3.4 (or the latest 3.3.x release) |
| Group | com.publicrecords |
| Artifact | publicrecords-api |
| Name | publicrecords-api |
| Description | UCC public records REST API |
| Package name | com.publicrecords.api |
| Packaging | Jar |
| Java | 21 |
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.javamv src/test/java/com/publicrecords/api/PublicrecordsApiApplicationTests.java src/test/java/com/publicrecords/api/PublicRecordsApiApplicationTests.javaWindows PowerShell uses Move-Item with the same paths.
Now replace the contents of the renamed main class with the proper class name:
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:
package com.publicrecords.api;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class PublicRecordsApiApplicationTests {
@Test
void contextLoads() {}
}
@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.
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.
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.
@Entitytells Hibernate "this Java class corresponds to a database row."@Table(name = "filings")sets the actual table name (otherwise it'd default tofiling).@Id+@GeneratedValue(IDENTITY)—idis 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+@Sizeare 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/filingNow create the file at the path shown in the header below, and paste the entire contents:
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; }
}
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
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 forFilingentities, withLongprimary keys." You inheritfindAll(),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 toSELECT * FROM filings WHERE state = ?. No implementation needed.
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);
}
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.
@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
FilingRepositoryis a constructor parameter. Spring sees this and passes the repository proxy in at startup. No@Autowiredneeded; one constructor is enough. @PathVariablebinds{id}in the URL to the method parameter.ResponseEntitylets us return a proper 404 when the filing isn't found, instead of throwing.
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());
}
}
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.
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/commonpackage 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);
}
}
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.propertiesWindows PowerShell: Remove-Item src\main\resources\application.properties
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@Entityclasses on startup, drops them on shutdown.sql.init.mode: always— runsdata.sqlafter 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.
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.
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');
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
@SpringBootTestspins up the full application context for the test — database, controllers, repository, the works. Slow but thorough.@AutoConfigureMockMvcgives us aMockMvcbean 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/filingpackage 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());
}
}
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.
"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).
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
| Symptom | Likely cause | Fix |
|---|---|---|
| Spring Boot fails: "Port 8080 already in use" | Another process owns 8080 | Kill it (lsof / Get-NetTCPConnection) or set server.port: 8081 in application.yml |
java -version shows 17 or 11 after install | JAVA_HOME points at the old JDK | Set JAVA_HOME to JDK 21 path; restart shell |
curl /filings returns [] | data.sql didn't load | Confirm 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 booting | Look for "Started PublicRecordsApiApplication" in server terminal |
mvn test fails with "package com.publicrecords... does not exist" | File in wrong package directory | Path must be src/main/java/com/publicrecords/api/<package>/ — check for typos |
claude command not found | npm global bin not in PATH | Run echo $PATH; add $(npm config get prefix)/bin if missing |
| Claude Code shows wrong project path | Launched from wrong directory | Exit, cd to project root, relaunch |
| Plan mode let Claude write a file | Mode toggled off mid-session | Check the bottom-right indicator; Shift+Tab to re-enter plan |
cat << EOF heredoc fails on Windows | Using Command Prompt instead of PowerShell 7+ | winget install Microsoft.PowerShell, then run pwsh |
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.
| Next | Module | You'll be able to… |
|---|---|---|
| CC1 | API Essentials | Models, Messages API, system prompts, streaming, structured output |
| CC2 | Prompt Engineering | Clarity, specificity, XML tags, few-shot, thinking triggers |
| CC3 | CLAUDE.md & Memory | Configure Claude per repo + Auto Memory |
| CC4 | Permissions & Sandbox | Pick the right safety mode + lock it down at OS level |
| CC5 | Skills & Slash Commands | Build your own /deploy, /triage, /review |
| CC6 | Subagents | Delegate research / review to specialists w/ their own tools |
| CC7 | Hooks | Auto-format, block dangerous commands, route via HTTP |
| CC8 | Tool Use Deep Dive | The tool-use loop, schemas, fine-grained, built-in tools |
| CC9 | MCP — Consumer & Server | Wire in GitHub/Postgres + build your own MCP server |
| CC10 | Claude Features | Thinking, prompt caching, citations, image/PDF, code-exec |
| CC11 | Evaluation | Test datasets, code & LLM-as-judge graders, CI gates |
| CC12 | RAG | Chunking, embeddings, BM25, hybrid search as MCP/subagent |
| CC13 | Workflows & Computer Use | Chaining/parallel/routing, agent loops, GUI automation |
| CC14 | Power User & CI/CD | Two-Claude review, worktrees, GitHub Actions, headless |
Knowledge Check
Five questions. Answer each, then click to reveal.
1. Which of the following is the strongest mental model for Claude Code?
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?
3. You're starting a long session in a new repo. Which command should you run first?
/clear/compact/init — generates a starter CLAUDE.md/hooks/init inspects the repo and produces a CLAUDE.md scaffold — the starting point for project-specific config (covered in CC1)./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?
claude --continueclaude --resume — pick the named session from the listclaude --newclaude /restore--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.--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?
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.