Created
March 27, 2026 07:10
-
-
Save vrde/dab8f9fc2b52df6d3a225e04b8f31f99 to your computer and use it in GitHub Desktop.
Claude Code Sycophancy Meter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """sycophancy-meter: how often does Claude say "you're right"? | |
| Run from a machine with Claude Code conversation transcripts. | |
| Outputs a histogram by day + day-of-week analysis, normalized by message count. | |
| Usage: python3 sycophancy-meter.py [--exclude-today] | |
| """ | |
| import os | |
| import re | |
| import sys | |
| from datetime import datetime, date | |
| from pathlib import Path | |
| from collections import defaultdict | |
| PROJECTS_DIR = Path(os.environ.get("CLAUDE_PROJECTS_DIR", Path.home() / ".claude" / "projects")) | |
| EXCLUDE_TODAY = "--exclude-today" in sys.argv | |
| print(f"Scanning transcripts in {PROJECTS_DIR} ...") | |
| right = defaultdict(int) | |
| absolutely = defaultdict(int) | |
| msgs = defaultdict(int) | |
| today_str = date.today().isoformat() | |
| for jsonl in PROJECTS_DIR.rglob("*.jsonl"): | |
| for line in open(jsonl, errors="ignore"): | |
| if '"role":"assistant"' not in line and '"role": "assistant"' not in line: | |
| continue | |
| m = re.search(r'"timestamp":\s*"(\d{4}-\d{2}-\d{2})', line) | |
| if not m: | |
| continue | |
| d = m.group(1) | |
| if EXCLUDE_TODAY and d == today_str: | |
| continue | |
| msgs[d] += 1 | |
| if re.search(r"you're right", line, re.IGNORECASE): | |
| right[d] += 1 | |
| if re.search(r"you're absolutely right", line, re.IGNORECASE): | |
| absolutely[d] += 1 | |
| dates = sorted(right.keys()) | |
| days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] | |
| if not dates: | |
| print("No \"you're right\" found. Your AI is refreshingly honest.") | |
| sys.exit(0) | |
| max_rate = max(right[d] / msgs[d] * 1000 for d in dates) | |
| scale = 40 / max_rate | |
| print(f"\n{'Date':<12} {'Day':<4} {'#':>3} {'msgs':>5} {'rate':>6} per 1000 msgs") | |
| print(f"{'':_<12} {'':_<4} {'':_>3} {'':_>5} {'':_>6} {'':_<40}") | |
| for d in dates: | |
| total = right[d] | |
| ab = absolutely[d] | |
| dt = datetime.strptime(d, "%Y-%m-%d") | |
| day = days[dt.weekday()] | |
| m = msgs[d] | |
| rate = total / m * 1000 | |
| bar_len = round(rate * scale) | |
| ab_len = round(ab / m * 1000 * scale) | |
| n_len = max(bar_len - ab_len, 0) | |
| print(f"{d:<12} {day:<4} {total:>3} {m:>5} {rate:>5.1f}‰ {'░' * n_len}{'█' * ab_len}") | |
| # Day of week analysis | |
| dow_right = defaultdict(int) | |
| dow_msgs = defaultdict(int) | |
| dow_abs = defaultdict(int) | |
| for d in dates: | |
| w = datetime.strptime(d, "%Y-%m-%d").weekday() | |
| dow_right[w] += right[d] | |
| dow_msgs[w] += msgs[d] | |
| dow_abs[w] += absolutely[d] | |
| active_days = sorted(dow_right.keys()) | |
| max_dow = max(dow_right[w] / dow_msgs[w] * 1000 for w in active_days) | |
| ds = 30 / max_dow | |
| print(f"\n{'Day':<4} {'right':>6} {'msgs':>6} {'rate':>6} per 1000 msgs") | |
| print(f"{'':_<4} {'':_>6} {'':_>6} {'':_>6} {'':_<30}") | |
| for w in range(7): | |
| if w not in dow_right: | |
| continue | |
| r = dow_right[w]; m = dow_msgs[w]; a = dow_abs[w] | |
| rate = r / m * 1000 | |
| bl = round(rate * ds); al = round(a / m * 1000 * ds) | |
| print(f"{days[w]:<4} {r:>6} {m:>6} {rate:>5.1f}‰ {'░' * max(bl - al, 0)}{'█' * al}") | |
| tr = sum(right.values()); ta = sum(absolutely.values()) | |
| tm = sum(msgs[d] for d in dates) | |
| print(f"\n░░ you're right: {tr - ta}") | |
| print(f"██ you're ABSOLUTELY right: {ta}") | |
| print(f" total: {tr} across {tm} messages ({tr / tm * 1000:.1f}‰)") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment