AEM 6850
Empirical Methods for Applied Economists
Prof. Ariel Ortiz-Bobea
Tuesday, September 22, 2026
By the end of this session you can:
CLAUDE.md.git diff, before accepting it, and say where every value in the result came from.CLAUDE.md for the project and a read-only reviewer agent, and say what each one changes.| Chat | Cowork | Claude Code | |
|---|---|---|---|
| Where it runs | a web page | the desktop app | your terminal, in one folder |
| How context arrives | you paste or attach it | from the folder you connect | from the folder, plus a file of standing instructions |
| Reads your files | no | every file in the folder | every file in the repository |
| Edits your files | no; you paste its output | yes, directly | yes; each edit shown as a diff first |
| Runs commands | on Anthropic’s servers, on an attached file | on Anthropic’s servers | on your laptop: R, git, anything in the terminal |
| Runs your R script | no; you run it | no; you run it | it can, when the prompt says so |
| What you read afterwards | the script you pasted | git status, git diff |
the box that shows each edit, then git diff |
CLAUDE.md.NAs handled silently; a unit mismatch; an estimation call that differs a little from what you asked.stopifnot() on row counts, unique keys and value ranges; estimators tried on simulated data with known parameters; the merge and the regression call read by you.git restore puts a file back./clear command).CLAUDE.md, a file it reads at every launch: base R only, never touch data/raw/, relative paths, a clean session, a master script, seeds, sessionInfo() saved with results.git status clean before the prompt; afterwards the diff is exactly what the model did.git restore puts a file back.placements, no git yet. Once, in the Console: install.packages("rvest"), the R package that reads web pages.placements, inside ~/github. Do not tick Create a git repository. Create Project.install.packages("rvest").placements.claude --permission-mode manual, Enter.placements; the status line reads ⏸ manual mode on.1. Yes.git log --oneline.
❯ prompt: where you type⏸ manual mode on./exit, then this line again.
placements
placements❯: type here, Enter sends⏸ manual mode on| Key or command | Does |
|---|---|
| Enter | sends the prompt |
| Esc | interrupts; on a question, declines |
| ↑ | brings back the previous prompt |
/clear |
new conversation, same folder |
/exit (or Ctrl+C twice) |
leaves Claude Code |
| Tools → Terminal → New Terminal | a second tab for git |
Prompt to paste into Claude Code (captured run)

.gitignore first: read it, 1. Yes1. Yesgit log --oneline prints one commitcode/<name>.R.| Too big to check | Right-sized |
|---|---|
| “Do my analysis of the placements.” | “Write code/placements.R: the table on the placements page, saved as data/placements.csv, four columns.” |
| “Make a report on placements.” | “Add a column postdoc: TRUE when the position is a postdoc. Change nothing else.” |
| “Fix my script.” | “The 2015 rows are missing; change the line that filters the years, nothing else.” |
Prompt to paste into Claude Code (captured run)
Write code/placements.R. It reads the table "Recent job placements" at
https://business.cornell.edu/admissions/graduate/phd-aem/for-hire/ and saves
it as data/placements.csv with the page's four columns, year, name, position,
organization, one row per graduate. Use the rvest package for the HTML and
base R for the rest. Run it with Rscript code/placements.R; if it fails, fix
the script and run again. Then show me the first five rows and the number of
rows. Do not use git.Rscript.sum(read.csv("data/placements.csv")$year == 2026).
1. Yes: Enter writes it2.: stops asking about edits. Leave it.3. No or Esc: refusewanted <- c("year", "name", "position", "organization")
headers <- lapply(tables, function(tb) tolower(html_text2(html_elements(tb, "th"))))
hit <- which(vapply(headers, function(h) identical(h, wanted), logical(1)))
if (length(hit) != 1) {
stop("Expected exactly one placements table with columns ", ...)
}+. Read it top to bottom once.vapply() and identical() are two to ask about.git status)Prompt to paste into Claude Code (captured run)

- red: what goes. Read these first.+ green: what arrives1. Yes accepts; Esc refuses placements$postdoc <- grepl("post[- ]?doc", placements$position, ignore.case = TRUE)
+
+# Counted on the page on 2026-09-21: 16 rows for 2026; postdoc spellings
+# 15 + 9 + 1. When the page changes this stops the script: recount, then edit.
+stopifnot(sum(placements$year == 2026) == 16, sum(placements$postdoc) == 25)Prompt to paste into Claude Code (captured run)
What it printed after running the script again
country_patterns <- c(
"Australia" = "Monash|New South Wales|University of Sydney",
"Canada" = "Alberta|Calgary|Ottawa",
"China" = "Beijing|Peking|Renmin|Xi.an Jiaotong",
"Germany" = "Ludwig-Maximilian|DTUM",
... # sixteen countries in all
)
placements$country <- "United States"
for (ctry in names(country_patterns)) { ... }"Australia", "Canada", "China", and thirteen more: typed here."United States": typed here too, and given to every row that matches nothing. That is 77 of the 109.Prompt to paste into Claude Code (captured run)
The page does not state countries, so the lookup is a guess. Replace it: write
the distinct organizations to data/countries.csv with an empty country column
for me to fill in, read that file in the script, and set country to NA when
the organization is not in it or its country is empty. Change nothing else.
Run it and tell me how many rows have NA.data/countries.csv: 107 organizations, an empty country column, written once.NA until you fill it. The CSV claims nothing nobody checked.
/init reads the folder, writes a first draft.1. Yes.CLAUDE.md (excerpt; the full file is on the page)
# placements
## Style
- Base R. The only package is `rvest`, and only for reading HTML.
- A number or a URL that could change sits at the top of the script as a
named constant in capitals: `URL <- "..."`.
- Comments say why, not what.
## Facts and checks
- A fact that is not on the page and not in a data file does not go into
code. Put it in a file under `data/` that I fill in, and use `NA` until
I do.
- After a merge or a filter, check the row count with `stopifnot()`.
## Working rules
- Do not run scripts unless the prompt asks. I run them in RStudio.
- Do not use git unless the prompt asks. I commit.snake_case; file names say what the script does.URL <- "..." at the top, never a bare value below.Prompt to paste into Claude Code (captured run)
# Reads data/placements.csv. Writes nothing; prints a table with one row
# per year giving the number of placements and the number of postdocs.
PLACEMENTS <- "data/placements.csv"
count_by_year <- function(placements) {
...
# Every placement belongs to exactly one year, so the counts must add up.
stopifnot(sum(by_year$n_placements) == nrow(placements))
by_year
}CLAUDE.md.CLAUDE.md included. It returns a report, not its working.Use a subagent to review code/x.R against this rule: ... Report what it says. Do not edit anything..claude/agents/, like CLAUDE.md: a name, a description, the tools it may use, then its instructions..claude/agents/hand-check-reviewer.md
---
name: hand-check-reviewer
description: Reviews one R script against one rule from the request. Read-only.
tools: Read, Grep, Glob
---
You review one R script against one rule given in the request. Read the
script. Reply in three lines: the rule; the line of code that follows it
or breaks it, quoted; the verdict, "follows" or "breaks". Do not edit any
file. Do not propose a rewrite unless asked.tools: is the fence: it can read; it cannot edit or run anything.Prompt to paste into Claude Code (captured run, before the fix)
The subagent's report, condensed; the full report is on the page
Rule: every value in the country column must come from the page or from a
data file, never from a table typed into the code.
Line that breaks it (code/placements.R, lines 44-67): country_patterns <- c(
"Australia" = "Monash|New South Wales|University of Sydney", ...) and
placements$country <- "United States".
No data file is read anywhere; the only input is read_html(url) on line 10.
Verdict: breaks.git status is clean.Nine exercises on the session page, with solutions behind a click. All of them run in placements, never in a graded repository.
git restoreby_year.RCLAUDE.mdCount it on the page before you run it, read the diff before you accept it, and keep facts you supply in a data file.
Every command, key, prompt shape and check from today, one line each, on the session page, with the things that bite.
AEM 6850 · Session 9