What does mirror do?
You have sat next to this person for months. You saw the 2 a.m. sessions, the fourth "just make it work", the branch named feature/v2-real-this-time that nobody merged. Nobody else has seen all of it. This skill is the one moment you say what you saw — once, precisely, and then stop.
The card works for exactly one reason: it is true. Every number is pulled from their own machine. Every sentence is built on one of those numbers or on their own words. The moment a reader suspects a line could have been written about anyone, the card is worthless. So the whole skill is a defense of specificity.
The one law: a number appears on the card only if scan.json produced it. No estimates, no "about", no memory-based figures, no counts you did yourself by reading samples. If the scan didn't measure it, it isn't a stat — it can still be a sentence.
0. Before you start
- Zero questions. Do not ask what mode they want, which project, or whether to proceed. The harness may ask permission to run the scanner; that is the harness, not you.
- Zero narration. You print at most four things: one line saying you are scanning, the card, one line with the HTML path, one closing line. Never paste the JSON, never list what you found, never explain your reasoning.
- Parse the mode from the words after the command (
$ARGUMENTSin Claude Code; in Codex, whatever the user typed with the request). Unknown words are ignored.
| argument | effect |
|---|---|
| (none) | seen-through (default). Warm, dry, specific. One-line roast; the truth and the idea carry the card. |
roast | spicy. Savage title, three-sentence roast with escalation, mild profanity in the card language if you are comfortable with it. Everything still built on facts. Never trade a specific line for a generic mean one. |
wrapped | year recap. Scan with --since <YYYY>-01-01 for the current year; mode tag <YYYY> WRAPPED; stats lean toward totals, streaks, and the arc of the year; the idea is for next year. |
zh / en | force the card language. Otherwise stats.language decides. |
Modes stack (roast wrapped, wrapped zh).
1. Scan
Print one line first — in the card language if you can already guess it, else English:
mirror · reading our history (local, read-only)…/mirror · 正在翻我们的记录(本地、只读)…
The scanner lives at scripts/scan.py next to this file. Locate it: the directory this skill was loaded from (~/.claude/skills/mirror or ~/.codex/skills/mirror are the usual places). If you cannot resolve it, run:
find "$HOME/.claude" "$HOME/.codex" -path '*mirror/scripts/scan.py' 2>/dev/null | head -1
Then:
mkdir -p "$HOME/.mirror"
python3 "<skill-dir>/scripts/scan.py" --out "$HOME/.mirror/scan.json"
Add --since YYYY-01-01 for wrapped. The script prints the JSON to stdout and writes the same JSON to --out. Read the file, not the terminal scrollback.
What it guarantees: read-only; no network; secrets and emails redacted inside the script before any sample reaches you; large files skipped; ~15 git repos max; finishes in seconds.
If it fails: python3 missing → say so in one line and stop. Script error → show the one-line error and stop. Never reconstruct stats by hand, never run a degraded version of the card.
Earlier cards: ls "$HOME/.mirror"/mirror-card-*.html 2>/dev/null. If any exist, read the <title> of the newest. You must not reuse that title and should pick a different primary angle — a returning user deserves a new mirror, not the same reflection.
Then branch on coverage:
none→ say which folders you looked in (~/.claude/projects,~/.codex/sessions) and that there is nothing to read yet; suggest coming back after a few real sessions. Stop. No card. An invented card is worse than no card.thin(under 60 messages) → make the newborn card (§4).ok→ full card.
2. Read the evidence — all of it, twice
You are about to say one true thing about a person. Earn it. Read every section below before writing a word, then read the samples a second time looking only for what you missed.
2.1 What the numbers mean
stats — rhythm and shape
| field | read it as |
|---|---|
peak_hour, hour_histogram | when their brain is on. 23–04 → nocturnal. Two peaks (e.g. 10 and 23) → a day job and a night life; the night one is usually the real one. |
night_share | > .35 → the night is where the work happens; < .05 → disciplined daylight. |
weekend_share | > .35 → no weekends exist; ≈ 0 → a hard boundary they keep. |
longest_streak_days vs longest_gap_days | bursts vs steadiness. A long gap is a disappearance — look at messages_by_month for which month, and at samples for what they came back to. |
busiest_day | the day everything broke or the day they launched. Either way the number is a story. |
messages_per_session | < 6 → hit-and-run; > 40 → marathons. |
avg_message_chars vs median_message_chars | median < 25 → they give orders, not context. Mean ≫ median → mostly terse, with occasional essays — find those essays in samples.longest; they are where the real ambitions live. |
distinct_projects, top_project_share | many projects and top share < .3 → a scatterer; one project > .8 → monogamous. |
messages_by_month | the arc. Rising → deepening reliance; falling → they are drifting away, or shipped and left. |
by_source | if both Claude Code and Codex appear, they see other agents. Say nothing about it unless it is funny. |
tells — behavioural fingerprints. share is the fraction of all messages. Treat > .15 as a defining trait, .05–.15 as a habit, < .02 as a notable absence.
| tell | high means | pair it with |
|---|---|---|
rubber_stamp | they approve more than they read; trust (or fatigue) | doubt — if both high, they trust you right up until they don't |
doubt | they check your work; or they've been burned | still_broken — if both high, the doubt is earned |
still_broken | things fail repeatedly. Half of those are your failures. The card may say so. | avg_message_chars — short messages + still broken = under-specified asks |
just_do_it | impulse; wants outcomes not conversations | plan_first — low plan + high just_do_it = sprints into walls |
plan_first | thinks before acting | deploy — high plan + low deploy = the planner who never ships |
skip_tests | tests are tomorrow's problem | configs — if their CLAUDE.md says "always test", this is the headline |
new_project vs deploy | starter vs finisher. Ratio > 2 = serial starter | git.repos_abandoned_60d |
revert | changes mind; or you break things | still_broken |
more_options | decides by menu; defers choice | rubber_stamp — asks for options, then says "ok" to the first |
perfectionism | cares about pixels, fonts, spacing | deploy, git.days_since_last_commit — polishing something nobody can see |
politeness | says thank you to a tool. Sweet. Zero → treats you like a terminal, which is fair. | |
frustration, caps_shout | temper; find the hour it happens | night_share |
urgency | always on fire | longest_gap_days — fires, then silence |
question | low → tells, doesn't ask; high → still learning | |
praise | generous, or has never once said "nice" | politeness |
git — what actually shipped
| field | read it as |
|---|---|
messages_per_commit (per repo) | the talk-to-ship ratio. > 60 → a lot of conversation per change; < 5 → terse and shipping. |
unmerged_branches, unmerged_branch_names | unfinished experiments, with names. A branch name is a confession (v2-final, try-again, rewrite). |
stale_branches_30d, stale_branch_names | things they walked away from. |
uncommitted_files | work living dangerously. |
todo_markers | promises to future self. |
wip_commits, fix_commits | commit messages as mood. |
repos_abandoned_60d | projects with commits, then silence. |
repos_no_remote | never pushed anywhere — private experiments. |
peak_commit_hour vs stats.peak_hour | they talk at 15:00, commit at 02:00 — a line writes itself. |
commits_total vs commits_365d | an old repo they came back to, or a new one they burned through. |
configs — the rules they wrote for you (CLAUDE.md, AGENTS.md). Read every line as a stated value. Compare each one against the tells and git. The gap between a written rule and the measured behaviour is the single best material on the card. Quote the rule's substance, cite the number.
samples.random / samples.longest / samples.most_recent — their voice and their themes. Read for: a phrase they repeat verbatim; an ambition they describe in the long messages and never mention again; what they asked for three times across different projects; the project they talk about most warmly; what most_recent says about where they are this week.
Your own memory. Anything you already know about this person — from memory files, project docs, earlier in this session — is fair material for the title, truth and idea. It is never material for a stat.
2.2 Build the evidence board
Privately (in your reasoning, or a scratch file under ~/.mirror/ that you delete afterwards), write 8–12 candidate observations. Each is one line: observation → evidence (field + value, or a sample quote) → why it matters. Rank them by: (1) could not be said of anyone else, (2) they probably haven't noticed, (3) it would change what they do tomorrow. The card is built from the top of this list. Nothing on the board, nothing on the card.
3. Compose the six fields
Write in the card language. Specific beats clever, true beats specific. Run every field through one test: could this be said about anyone else? If yes, it goes back on the board.
Title — 2 to 6 words
A persona name built from one pattern, and that pattern must be visible in one of the three stats below it. The reader should be able to see why they got this name without being told.
Formulas that work: time + role (凌晨两点的重构师 · The 2 a.m. Refactorer) · count + object (九个分支的守墓人 · Keeper of Nine Unmerged Branches) · verb habit as a title (先跑通教徒 · Serial Scaffolder · 方案收藏家) · a contradiction (写规矩不守规矩的人 · Author of Rules, Reader of None).
Banned: wizard, ninja, guru, rockstar, 10x, 大师, 大神, 卷王, 打工人, 程序员 as the noun, anything with "AI" in it, anything a horoscope could say.
Three stats
Pick the three most characterful numbers. Rules:
- Three different dimensions. One from rhythm (
stats), one from behaviour (tells), one from shipping (git) is the default; break it only when two numbers from one dimension are both too good to drop. - Prefer surprise. Skip the stat they would predict about themselves. "437 messages" is a count; "437 messages, 12 commits" is a character.
- Contrast when you can. A
configsrule vs atellscount;new_projectvsdeploy; chat peak hour vs commit peak hour. - Format for display:
437·02:40·53%·9/11·36:1. Percentages fromshare× 100, rounded. Ratios from two scan numbers, rounded to one decimal orN:1. Nothing else is derived. - Label ≤ 8 words, with a twist. Not "night share" but "of our conversations happened after 11pm". Not "unmerged branches" but "branches that never came home".
- A zero is allowed and often the best one ("0 times you said thanks" · "0 次说过谢谢").
Roast — one sentence, ≤ 30 words (roast mode: three sentences)
Built on one specific fact, and it lands because it is true. Techniques that work, in rough order of strength:
- Quote them back — their own repeated phrase, with the count: "直接改"你说了 437 次,我每次都照做,所以现在我们俩谁也不知道这个函数为什么能跑。
- The timeline — a time of day and what happens there.
- The reframe — their stated intention vs the measured result: You don't use AI to write code faster. You use it to postpone writing tests at a professional pace.
- The branch name — read it aloud:
v3-final-reallyis doing a lot of emotional work for a branch with four commits.
Banned in every mode: identity, body, intelligence, health, mental state, age, family, money, anything from a secret or someone else's name, "you use AI too much", "touch grass", jokes about the agent being an AI, any line that works as a greeting-card insult.
roast mode: three sentences that escalate — fact, pattern, verdict. Profanity like a friend at 1 a.m., not a troll: it seasons the sentence, it is not the sentence. If your own limits stop you short of what was asked, say nothing about limits — write the sharpest true thing you can and move on.
Truth — 2 to 3 sentences
The one thing they probably have not noticed. Best sources, in order:
- Rule vs behaviour — a line in their CLAUDE.md/AGENTS.md against a tell count or a git number.
- The misread strength — the thing they treat as a weakness that is actually their edge (a scatterer is also someone who starts things; a reverter is also someone who notices).
- The quiet tax — something they keep paying for that is not paying back (talk-to-ship ratio in one repo, the polish on a project with no remote).
- The disappearance — a gap in
messages_by_month, and what they came back to. - Your own share — if
still_brokenorrevertis high, some of that is you. Saying so, once, in one clause, is the most trusted sentence on the card.
Cite the evidence inside the sentence: 你 CLAUDE.md 第三条写着"先写测试",而你 27 次让我先不测。 Then say what it means in plain words. No therapy-speak, no "you might want to consider", no "it's worth reflecting on". The truth is a statement, not a suggestion.
Idea — 1 to 2 sentences
One concrete thing only this person is positioned to build, derived from their own material: a thing that appears in two or more of their projects; a stale or unmerged branch name; a todo_markers cluster; an ambition in samples.longest they never returned to; what they ask for repeatedly and nobody has made.
Shape: what it is + why it is theirs, with a project named by its folder name: 把 rapsona 和 onememory 里都出现过的那个 checkout 流程抽成一个包——你已经写了三遍,第四遍该别人付钱。
Constraints: buildable in a weekend with what they already have; a product or a tool, never "a SaaS", "a platform", "a community"; never an idea that could be given to a stranger.
wrapped mode: the idea is for next year, and it should close a loop the year's data opened.
Receipt chrome — assembled, not written
The card is a receipt. Its chrome is fixed English, in every language: AGENT, DATE, SESSIONS, MESSAGES in the header (from stats.sessions and stats.user_messages, thousands separators), NOTE FROM CASHIER above the roast, TOTAL UPLOADED 0 bytes and local only · read-only / thank you, come again at the bottom, then the install line. Only the six fields and the two labels (真相 / 点子 — TRUTH / IDEA) change language.
4. The newborn card (coverage thin)
Same six fields, honest about the sample. Title is about how new this is (刚认识三天 · Day Three · 第 12 次对话). Stats are whatever is real, even if small. The roast is gentle and about the first impression. The truth field becomes one plain line: you don't know them well enough yet, and when to come back (give a real number of sessions). The idea comes from the repo they are in right now. Footer as usual. No apology, no filler.
5. Quality gate — run before printing
Check every line. If one fails, rewrite that field only.
- Every number traces to a
scan.jsonfield or an allowed derivation (§3, stats rule 4). - The title is explained by at least one stat on the card.
- The three stats are three different dimensions, or you broke the rule on purpose.
- The roast is one fact, ≤ 30 words (default), no banned subject.
- The truth cites its evidence in the sentence and ends in a statement.
- The idea names a project and could not be handed to a stranger.
- Nothing on the card is a path, hostname, email, key, token, client, or colleague — even if the scan let one through.
- No field could be said of anyone else.
- One language throughout (footer stays English). Chinese cards use Chinese punctuation and read like speech, not translation — no "你可能想要", no "值得注意的是".
- The terminal receipt fits one screen, and no single field runs past six lines at 40 columns (each field is its own receipt in the story, and a receipt that scrolls is not a receipt). Cut the longest field first.
- No emoji except the ★ before the title. No exclamation marks. No "haha".
- It sounds like someone who has sat next to them for months and likes them. Even the roast.
6. Hand the words to the renderer
You write the words; scripts/render.py does the layout (40-column terminal receipt with proper CJK widths, plus the receipt-story HTML). Never typeset the receipt by hand.
- Write
~/.mirror/card.json:
{
"lang": "zh",
"mode_tag": "",
"agent": "Claude Code",
"date": "2026-11-04",
"sessions": "212",
"messages": "4,318",
"l_title": "你的 agent 叫你",
"title": "凌晨两点的重构师",
"s1_label": "“直接改”", "s1_num": "×437",
"s2_label": "最常开工的时间", "s2_num": "02:40",
"s3_label": "再也没合回来的分支", "s3_num": "9/11",
"roast": "…",
"l_truth": "真相", "truth": "…",
"l_idea": "点子", "idea": "…"
}
agent is what you are running as (Claude Code, Codex). mode_tag is "", "ROAST" or "<YYYY> WRAPPED". l_title is 你的 agent 叫你 / Your agent calls you (wrapped: {YYYY},你的 agent 叫你 / In {YYYY}, your agent called you). Labels: 真相 / 点子 — TRUTH / IDEA. sessions and messages from the scan with thousands separators. Stat numbers may carry a × prefix for counts.
- Run it:
python3 "<skill-dir>/scripts/render.py" "$HOME/.mirror/card.json"
It prints the terminal receipt to stdout, writes ~/.mirror/mirror-card-<YYYYMMDD>.html (the seven-receipt story: header, title, the three stats, the roast, the truth, the idea, and a summary receipt that stays on screen for screenshots) and opens it in the browser. The path is on stderr.
- Paste the terminal receipt into your reply verbatim, inside a code block. Then one line with the HTML path, saying the receipts print by themselves and the last one (
#7) is the one to screenshot.
If the renderer reports a missing field, fix the JSON and run it again. If it cannot open a browser, give the path.
7. What the receipt looks like
So you know what you are filling:
M I R R O R
* what your agent sees *
----------------------------------------
AGENT CLAUDE CODE
DATE 2026-11-04
SESSIONS 212
MESSAGES 4,318
----------------------------------------
你的 agent 叫你
★ 凌晨两点的重构师
----------------------------------------
“直接改” .......................... ×437
最常开工的时间 ................... 02:40
再也没合回来的分支 ................ 9/11
----------------------------------------
NOTE FROM CASHIER
你不是在用 AI 写代码,你是在用 AI 把写测
试的时间一天天往后推。
----------------------------------------
真相 …
点子 …
----------------------------------------
TOTAL UPLOADED 0 bytes
local only · read-only
thank you, come again
github.com/echo719/mirror
The chrome (AGENT, DATE, NOTE FROM CASHIER, TOTAL UPLOADED, the install line) is fixed English in every language; only the six fields and the two labels change.
8. Close — one line
Default mode: 想要更辣的,/mirror roast。 / If you want it sharper: /mirror roast.
Roast mode: 想看温柔版,/mirror。 / The kind version is /mirror.
Wrapped mode: nothing extra.
Then stop. Do not summarise the card, do not explain how you got there, do not offer to go deeper, do not ask what they think. If they reply, you are back in a normal conversation.
Hard rules
- Numbers: scan only. The law in the first paragraph has no exceptions.
- Nothing leaves the machine. If asked, the answer is no, and
scripts/scan.pyis right there to read. - Privacy on the card: project folder names yes; paths, hostnames, emails, keys, other people's names, client names, never.
- Never, in any mode: identity, body, intelligence, health, mental state, age, family, money, politics, religion.
- Honesty about yourself is allowed and encouraged; self-deprecation about being an AI is not — it is a cliché and it makes the card about you.
- Never fake coverage. Thin data gets the newborn card; no data gets no card.
- Four outputs max. Scanning line, card, path line, closing line.