Install a project log in any Claude Code session in 10 minutes
Copy-paste setup so Claude automatically records non-trivial fixes, decisions, and retros in your repo — without hallucinating.
Claude Code sessions vaporize. The session ends, the chat scrolls off, and three weeks later you can't reconstruct why you picked that library, what the error message actually was, or which commit fixed the production bug. The transcript is technically saved, but nobody greps a transcript.
The fix is small: have Claude write a structured log entry every time something non-trivial happens, in two files at once, with rules that block fabrication. Below is the exact setup. Copy-paste, no thinking required. About 10 minutes from zero to working.
This is the same system this site runs. The /projects/daeseon-ai page on this domain is a live demo — every entry there was written by Claude under these rules, including the meta-entry about designing the system.
What you'll have when you're done
Two files per non-trivial event, written by Claude in the same turn as the fix commit:
docs/troubleshooting.md— flat, problem-indexed, terse. The thing you grep when you hit the same bug again. Symptom / Cause / Fix / Commit / Pattern.content/logs/<project-slug>/<YYYY-MM-DD>-<short-slug>.mdx— narrative, date-indexed, project-scoped. Carrieskind(troubleshoot / tech-retro / ux-retro / business / monetization / update) andvisibility(public / unlisted / private).
Plus a Stop hook that prints a reminder after any recent commit so Claude doesn't quietly skip the log.
Prerequisites
- Claude Code installed and running in your project
- git repo (any host)
- jq on PATH (preinstalled on macOS; `apt install jq` on Linux)That's it. Doesn't matter if you're on Next.js, Rails, FastAPI, Go, or anything else.
Fastest path (one-liner)
If you trust the script (read it first — less it from the same URL), this does everything in steps 1, 2, and 4 in one shot. It does NOT touch your CLAUDE.md — that's the only manual step.
cd /path/to/your/repo
curl -fsSL https://raw.githubusercontent.com/Daeseon-AI-Factory/daseon-blog/main/install/setup.sh | bashWhat it does:
- Creates
docs/,.claude/hooks/, andcontent/logs/<your-repo-name>/ - Downloads
docs/troubleshooting.md(the seed file) - Downloads
.claude/hooks/stop-check.sh(the positive-trigger v3 hook script) - Downloads
.claude/settings.json(wires the script to Claude Code's Stop event) - Prints the
CLAUDE.mdsnippet so you can read and paste it manually - Leaves your existing files alone (idempotent — won't overwrite)
If you'd rather see each step explicitly, keep going below.
Step 1 — Make the directories
In your repo root:
mkdir -p docs content/logs/$(basename $PWD) .claude/hooks$(basename $PWD) uses your repo folder name as the project slug. That's fine for standalone use.
If this repo is going to be aggregated by a portfolio site, the slug must match what the portfolio expects — which is not necessarily the repo name. Check the portfolio's content/projects/<slug>.mdx files for one whose frontmatter repo: matches this repo's git remote get-url origin. Use that filename (minus .mdx) as your slug. Repo name and slug can diverge (e.g. repo ddalkkak → slug dalkkak-ai). If no portfolio entry exists yet, $(basename $PWD) is a sensible default and the portfolio can be wired later with the same name.
Whatever you pick: kebab-case, no spaces, used identically in the directory name and in every entry's project: frontmatter field.
Step 2 — Seed docs/troubleshooting.md
One-line download:
curl -fsSL -o docs/troubleshooting.md \
https://raw.githubusercontent.com/Daeseon-AI-Factory/daseon-blog/main/install/troubleshooting-starter.mdOr save this content manually:
# Troubleshooting log
Issues hit and the fix for each. Newest at the bottom.
Format: **Symptom** · **Cause** · **Fix** · **Commit** · (optional **Pattern**).
When you fix a non-trivial issue, append an entry below. The `.claude/settings.json` Stop hook will remind you after any recent commit.
---
## How to add a new entry
```markdown
## <short title>
- **Symptom**: <literal error message or observable behavior>
- **Cause**: <verified explanation> (or `Hypothesis: ... Verified by: ...`)
- **Fix**: <files/functions changed, mechanism>
- **Commit**: <hash from `git rev-parse HEAD` AFTER committing>
- **Pattern**: <one-line recurring lesson — optional>
```
Concrete only. No "lessons learned" essays.That's the starter. Real entries get appended below ---.
Step 3 — Add the CLAUDE.md rule
Fetch the snippet text, read it, then append it to your CLAUDE.md:
curl -fsSL https://raw.githubusercontent.com/Daeseon-AI-Factory/daseon-blog/main/install/claude-md-snippet.md
# review the output, then append it to CLAUDE.mdOr paste this block directly (create the file if it doesn't exist):
## Project log (required, dual-write)
When you fix or decide something non-trivial in this repo, write BOTH of these in the same turn as the commit:
1. `docs/troubleshooting.md` — terse problem-indexed reference (Symptom / Cause / Fix / Commit / Pattern). Append a new entry below the `---` divider.
2. `content/logs/<project-slug>/<YYYY-MM-DD>-<short-slug>.mdx` — dated narrative with frontmatter:
```yaml
---
title: "Concrete one-line title"
date: "YYYY-MM-DD"
project: "<project-slug>"
kind: "troubleshoot | tech-retro | ux-retro | business | monetization | update"
visibility: "public | unlisted | private"
language: "en"
summary: "One or two sentences."
tags: ["topic", "stack"]
---
```
### What counts as non-trivial
LOG IT: build/deploy errors, hidden coupling, dependency migrations, architecture or infra decisions, design/copy choices made on judgment, strategy or pricing memos.
DON'T LOG: routine renames, lint fixes, typo fixes, dependency bumps with no behavior change, formatting commits.
### Anti-hallucination rules (non-negotiable)
1. **Symptom is literal.** Paste the actual error/output in a fenced code block. No paraphrasing.
2. **Cause is verified.** Only state what you read in the actual code or ran in the actual command. If you guessed, write `Hypothesis: ...` and `Verified by: ...`. If unverifiable, omit Cause or mark `Suspected:` with an explicit caveat.
3. **Fix names actual files.** `git diff` is the source of truth. If `git diff` doesn't show the change, don't claim you made it.
4. **Commit hash AFTER committing.** Use `git rev-parse HEAD` after the commit lands. Never write a hash that doesn't exist yet.
5. **Date from git.** `git log -1 --format=%cI` for the commit time. For forward-looking entries (decisions being written in the moment), today's date from the session start. Never guess.
6. **Pattern is rare.** Only write a Pattern line if a recurring lesson is obvious from this one incident. Padding it with generic advice is worse than omitting.
7. **No fabricated metrics.** "Took about 60s" if you saw 60s. "Took 1m 23s exactly" only if you have the timestamp.
### Visibility defaults by kind
- `business`, `monetization` → `private` by default (strategy memos shouldn't ship accidentally)
- `knowledge`-style facts → `unlisted` if you have such a type
- Everything else → `public`
Override per entry in frontmatter.
### Format requirements (non-negotiable)
- **Quoted YAML dates.** Always `date: "2026-05-31"`. Never `date: 2026-05-31` (unquoted). Unquoted ISO date literals get parsed as `Date` objects by some YAML libraries and break downstream MDX rendering on portfolio sites.
- **Slug = portfolio's expectation.** The `project:` value must match wherever this repo is aggregated (see Step 1).
### Writing voice
Log entries are plain descriptions, not essays:
- No `I learned that...` / `the lesson here is...` conclusions. No engagement bait. No performative wisdom.
- No industry-jargon dressing — don't reframe a basic bug as "primitive obsession" or rename `data acquisition server` to "ISA-95 Level 2 SCADA layer" unless that's already how you'd say it in conversation.
- Describe system structure as simple boxes-and-arrows in prose (A → B → C), not domain vocabulary.
- Frustration in the moment is fine to describe; don't dress it up as "the lesson here was patience".This is the instruction Claude reads at the start of every turn (via CLAUDE.md), so it now has a permanent rule that says "log everything non-trivial, in two files, with verified facts."
Step 4 — Add the Stop hook
Two files: the hook script itself, and the Claude Code settings that wire it to the Stop event. The script is in a separate file (rather than inline JSON) because it has three positive-trigger checks and is easier to maintain that way.
curl -fsSL -o .claude/hooks/stop-check.sh \
https://raw.githubusercontent.com/Daeseon-AI-Factory/daseon-blog/main/install/hooks/stop-check.sh
chmod +x .claude/hooks/stop-check.sh
curl -fsSL -o .claude/settings.json \
https://raw.githubusercontent.com/Daeseon-AI-Factory/daseon-blog/main/install/settings.jsonIf .claude/settings.json already exists with other settings, merge in this single hook entry rather than overwriting:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/stop-check.sh",
"timeout": 10
}
]
}
]
}
}What the hook does
After Claude finishes a turn, the script runs. It checks the latest commit (if any in the last 3 minutes) and decides whether to block or pass.
The five branches, in order:
- No recent commit (>3 min ago) → pass silently. Avoids stale-repo false positives.
- Latest commit hash already appears in
docs/troubleshooting.mdor anycontent/logs/file → pass. The work is logged. - Positive trigger fires → BLOCK. Three triggers, any of which is enough:
- LOC delta > 200 (insertions + deletions across all files)
- Touches sensitive paths:
lib/*storage*,lib/*auth*,lib/*hooks*,middleware.*,app/(admin)/*,app/api/auth/*,.claude/settings*,.claude/hooks/*,install/*,next.config*,package.json,tsconfig*,migrations/*,prisma/schema*,*.schema.* - Subject keywords (case-insensitive):
decision,architecture,fallback,audit,auth,security,migration,dispatcher,ADR-N,refactor,pivot,breaking,deprecat,hidden coupling
[no-log]or[skip-log]in commit subject AND no trigger fired → auto-append<!-- skipped: <hash> <subject> -->todocs/troubleshooting.md, pass.- Default → BLOCK. Require either a full dual-write entry or a
[no-log]tag on the commit.
Why positive triggers (v3)
Earlier versions of this hook trusted the author's [no-log] tag. Auditing the system after five months of use revealed dozens of commits where the author had tagged [no-log] on changes that were genuinely non-trivial — 800-LOC admin features, 300-LOC security additions, ADR-level architecture decisions buried in docs/. The [no-log] mechanism is necessary (most commits really are routine), but author judgment slips, and the system has no way to catch that.
The v3 hook adds positive triggers that override [no-log]. If a commit touches a sensitive path, exceeds 200 LOC, or carries an architecture-flavored keyword, the hook blocks the turn regardless of how the commit was framed. This is the layer that catches what author judgment misses.
False positives — the override mechanism
Triggers will sometimes fire on genuinely routine commits (a 250-LOC dependency-graph cleanup, an auth-keyword commit that's really just a typo fix in an auth README). To unblock without a full dual-write entry, append this line to docs/troubleshooting.md:
<!-- override-trigger: <hash> <subject> — <real rationale> -->The override is human-visible and the rationale must be a real sentence — "false positive" alone isn't enough; explain why this commit is routine despite the trigger. Silent overrides are deliberately blocked.
If overrides become frequent, the trigger threshold or regex is wrong. Tighten or loosen it in .claude/hooks/stop-check.sh.
Committing the hook
Commit both files to the repo deliberately. Other machines that clone get the same hook automatically.
Step 5 — Trigger the first real entry
Have Claude do something non-trivial — fix a real bug, make an architecture decision, anything that should be logged. After the fix commit, the Stop hook will print the reminder. Claude should then write both files.
To make sure it's working, ask Claude explicitly the first time:
We just fixed <X>. Per CLAUDE.md rules, write the troubleshooting entry
and the log mdx file in the same turn as a follow-up commit. Use the
seven anti-hallucination rules — paste the actual error, name actual
files, use the real commit hash from `git rev-parse HEAD`.Verify:
# The new entry should be at the bottom of troubleshooting.md
tail -20 docs/troubleshooting.md
# The new mdx should exist
ls -la content/logs/$(basename $PWD)/
# The commit hash in the entry should match
git log -1 --format=%H | cut -c1-7If the entry fabricated something (e.g., wrong commit hash, plausible-sounding cause that doesn't match the code), point Claude at the specific rule it violated and ask for a correction. The rules only work if you enforce them on day one — Claude is happy to take shortcuts otherwise.
Step 6 — Watch it compound
After 5–10 entries you'll have:
- A grep-able problem cache (
docs/troubleshooting.md). When you hit a familiar-looking error, grep here first. - A project timeline (
content/logs/<slug>/). Sorted chronologically, you can read the build narrative from the first decision to the latest commit. - A real record of judgment calls (kind=tech-retro, ux-retro, business). These are the things you'd otherwise forget within a month.
If you have a blog/site/portfolio, render the public entries on a project page. If you don't, the markdown alone is plenty — cat docs/troubleshooting.md | less works fine as a CLI lookup.
Step 7 — Aggregate multiple repos into one portfolio (optional)
If you have several projects across separate repos and want one portfolio site (this blog) to render all of their timelines, this site supports cross-repo aggregation. Each project repo keeps its own logs in its own repo — no syncing, no copying.
How to wire it:
- In the portfolio repo's
content/projects/<slug>.mdx, addlogSourceRepo: "owner/name"to the frontmatter (or use/admin/projectseditor — the field is right under "Repo URL"). - The portfolio's
GITHUB_TOKENneedsContents: readaccess on that source repo. Same-owner repos work with the existing PAT; different owner needs a new fine-grained PAT. - Visit
/projects/<slug>on the portfolio — the timeline now lists entries fetched from the source repo'scontent/logs/<slug>/directory in real time.
Notes:
- It's pull-on-demand, not polling. The portfolio fetches when someone visits, with a 30-second ISR cache. No webhooks, no GitHub Actions, no scheduled jobs.
- Source repos don't need any extra setup beyond the standard install (steps 1-4). They just need
content/logs/<slug>/*.mdxfiles. - Visibility is per-entry.
visibility: publicshows in the timeline.privatedoesn't appear anywhere on the portfolio.unlistedis reachable by direct URL only. - Each portfolio is single-owner. Friends adopting the spec render their own portfolio fetching their own repos. There's no central aggregator.
Human commentary alongside AI entries (.human.mdx)
The portfolio detail page for each log entry renders two columns side-by-side on desktop: the AI version (from the source repo) on the left, and an optional human commentary on the right. If no commentary exists, the right column shows "Review needed" — making the gap visible.
The commentary file is <event>.human.mdx, body-only (no frontmatter required). It lives in the portfolio repo, not the source repo. Path: <portfolio>/content/logs/<slug>/<event>.human.mdx. The author writes it in their own voice — typically days or weeks after the AI entry — as a parallel read-through. The portfolio renders both together; the source repo is never edited.
This means each AI artifact stays as a permanent record of what AI produced, and the human commentary is additive. Don't create .human.mdx files in source repos.
Bilingual logs (optional)
Two patterns both supported:
- Pair per language:
<event>.en.mdx+<event>.ko.mdxnext to each other incontent/logs/<slug>/, with matchingproject:and the respectivelanguage:set in each. The portfolio renders the file matching the visitor's locale. - Single file + frontmatter language:
<event>.mdxwithlanguage: "en". Simpler for English-only.
The portfolio filters timeline entries by visitor locale automatically. A satellite that only writes English shows the same entries on both /projects/<slug> and /ko/projects/<slug> (fallback).
Backdating an entry
If something important happened months ago and you only just decided to log it, set the date frontmatter to the actual event date:
---
title: "PL/SQL refactor — when we hit the costing bottleneck"
date: "2024-11-03"
project: "manufacturing-cost-system"
kind: "tech-retro"
visibility: "public"
language: "en"
summary: "Retro on cutting 7K lines to 3K. Written 18 months after the fact."
---The timeline sorts by date ascending, so backfilled entries slot into the right historical position. The 7-rule anti-hallucination guidance still applies — only write what you can verify (from git history, old notes, your memory of specific commit hashes). If exact details are missing, write [unverified: <claim>] rather than inventing them.
FAQ
"My project isn't Next.js — does this work?"
Yes. Nothing about the system depends on a web framework. You're writing markdown files. If you have no website to render them on, drop the mdx and use .md in docs/logs/<project>/<date>-<slug>.md instead. Same structure, same rules.
"What if I have multiple projects in one repo (monorepo)?"
Use the directory structure — content/logs/api/..., content/logs/frontend/..., content/logs/infra/.... The project field in frontmatter matches the directory.
"Claude keeps writing entries for trivial stuff."
Tighten the CLAUDE.md rule. Add explicit examples of what NOT to log. After 2–3 corrections it usually settles.
"Claude keeps writing entries that paraphrase the error instead of pasting it."
Quote rule #1 verbatim back at it: "Symptom is literal. Paste the actual error in a fenced code block. No paraphrasing." Reject the entry and ask for a rewrite. Once it's done correctly twice in a row, the pattern locks in.
"What's the point of dual-write? Isn't one file enough?"
I tried single-source. Date-indexed only (the mdx logs) → grep returns the fix buried in narrative. Slow recall. Problem-indexed only (troubleshooting.md) → no timeline, no public archive of decisions, no recruiter-readable artifact. The duplication cost is low because the mdx body is mostly a richer copy of the troubleshooting entry. It pays back the first time a bug recurs.
"Can I disable the Stop hook reminder?"
Delete .claude/settings.json (or its Stop section). You'll lose the nudge — Claude will sometimes skip logging without a prompt. Recommended to keep it.
"Where do I put this on a team?"
The CLAUDE.md rule and .claude/settings.json should both be committed to the repo. Anyone who clones gets the same Claude instructions and the same hook. The dual-write happens regardless of who's at the keyboard.
What this is NOT
- It's not a replacement for ADRs (architecture decision records) in larger orgs. ADRs have review gates and a dedicated repo. For solo/small projects, this is plenty.
- It's not a project management tool. Issues and tickets belong in GitHub Issues / Linear; this log records what happened, not what's planned.
- It's not an analytics or metrics system. It's a writeup of judgment calls and incidents, not numbers.
The minimum viable version
If 10 minutes is too much, skip steps 2 and 4. Keep only step 3 (the CLAUDE.md rule) and start writing to docs/log.md (one flat file). You'll lose the Stop hook reminder and the structured mdx surface, but Claude will still log non-trivial events when prompted. Add the rest later.
The thing that actually compounds value is rule #2 (Cause is verified). If you only keep one anti-hallucination rule, keep that one. It's the difference between a notebook that's worth re-reading and a notebook that quietly fills up with plausible-sounding fiction.
This site uses exactly this setup. All copy-paste files live in the install/ directory of the repo:
setup.sh— one-shot bootstraptroubleshooting-starter.md— seed filesettings.json— Stop hookclaude-md-snippet.md— rule block forCLAUDE.md
Repo: github.com/Daeseon-AI-Factory/daseon-blog. The portable spec lives at docs/project-log-spec.md. Live timeline demo: /projects/daeseon-ai.