I got invited to interview at a major AI company.
I asked Claude Code to draft my reply with available time slots. Claude produced five neatly formatted dates with weekday labels and time ranges.
Looked perfect. I hit send.
Hours later, the recruiter replied politely:
"June 2nd is a Tuesday, not a Monday. Could you please double-check your availability?"
All five weekday labels were wrong.
Why LLMs Cannot Calculate Day-of-Week
This is not a Claude-specific bug. It happens with GPT, Gemini, and every other LLM.
The reason is fundamental. An LLM is a statistical next-token predictor, not a calendar. Training data contains sentences like "Today is Monday," "Today is Tuesday," etc. with roughly equal frequency. For the model, each weekday has approximately 1/7 probability — it is essentially rolling a die.
| Concept | Analogy |
|---|---|
| LLM weekday calculation | Rolling a 7-sided die |
new Date().getDay() |
Looking at a calendar |
| A Hook | Checking the calendar before Claude rolls the die |
This is well-documented on GitHub:
- #17338 — "Claude always uses wrong weekdays"
- #24466 — "consistently off by one day"
- #2618 — "A date tool should be included by default"
Multiple people I know have independently confirmed: Claude always gets the days wrong.
The Fix: A Date-Weekday Verification Hook
Claude Code's Hooks system lets you intercept and validate content before Claude writes it to disk.
This hook:
- Intercepts
WriteandEdittool calls - Scans content for date + weekday patterns using regex
- Computes the actual weekday using
new Date().getDay() - If claimed ≠ actual → blocks the tool call (exit code 2)
Supported Patterns
| Format | Example | Language |
|---|---|---|
| Full month | June 2 (Tue) |
English |
| Abbreviated | Jun 2 (Tue) |
English |
| Slash format | 6/2 (Tue) |
English |
| Japanese | 6月2日(火) |
Japanese |
Installation
Step 1: Save the Hook
Save as ~/.claude/hooks/date-weekday-verifier.js:
#!/usr/bin/env node
const MONTH_NAMES = {
january: 0, february: 1, march: 2, april: 3, may: 4, june: 5,
july: 6, august: 7, september: 8, october: 9, november: 10, december: 11,
jan: 0, feb: 1, mar: 2, apr: 3, jun: 5,
jul: 6, aug: 7, sep: 8, oct: 9, nov: 10, dec: 11,
};
const EN_DAY_NAMES = {
sun: 0, sunday: 0, mon: 1, monday: 1, tue: 2, tuesday: 2, tues: 2,
wed: 3, wednesday: 3, thu: 4, thursday: 4, thur: 4, thurs: 4,
fri: 5, friday: 5, sat: 6, saturday: 6,
};
const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
function verify(content) {
const errors = [];
const year = new Date().getFullYear();
let match;
// "Month Day (Weekday)" e.g. "June 2 (Mon)"
const p1 = /\b(january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|jun|jul|aug|sep|oct|nov|dec)\s+(\d{1,2})\s*\((\w+)\)/gi;
while ((match = p1.exec(content)) !== null) {
const month = MONTH_NAMES[match[1].toLowerCase()];
const day = parseInt(match[2], 10);
const claimed = EN_DAY_NAMES[match[3].toLowerCase()];
if (month === undefined || claimed === undefined) continue;
const d = new Date(year, month, day);
if (d.getMonth() !== month || d.getDate() !== day) continue;
if (d.getDay() !== claimed)
errors.push({ text: match[0], claimed: DAY_LABELS[claimed], actual: DAY_LABELS[d.getDay()] });
}
// "M/D (Weekday)" e.g. "6/2 (Mon)"
const p2 = /\b(\d{1,2})\/(\d{1,2})\s*\((\w+)\)/g;
while ((match = p2.exec(content)) !== null) {
const m = parseInt(match[1], 10) - 1, day = parseInt(match[2], 10);
const claimed = EN_DAY_NAMES[match[3].toLowerCase()];
if (claimed === undefined || m < 0 || m > 11) continue;
const d = new Date(year, m, day);
if (d.getMonth() !== m || d.getDate() !== day) continue;
if (d.getDay() !== claimed)
errors.push({ text: match[0], claimed: DAY_LABELS[claimed], actual: DAY_LABELS[d.getDay()] });
}
return errors;
}
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (c) => { input += c; });
process.stdin.on('end', () => {
try {
const h = JSON.parse(input);
if (!['Write', 'Edit'].includes(h.tool_name)) { process.exit(0); return; }
const ti = h.tool_input || {};
const content = ti.content || ti.new_string || '';
if (!content || content.length < 5) { process.exit(0); return; }
const errors = verify(content);
if (!errors.length) { process.exit(0); return; }
const list = errors.map(e =>
' WRONG: "' + e.text + '" -- claimed ' + e.claimed + ', actually ' + e.actual
).join('\n');
console.error('DATE VERIFIER: ' + errors.length + ' wrong weekday(s)!\n\n' + list + '\n\nFix before proceeding.');
process.exit(2);
} catch { process.exit(0); }
});
Step 2: Register in settings.json
Add to ~/.claude/settings.json under hooks.PreToolUse:
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "node \"~/.claude/hooks/date-weekday-verifier.js\"",
"timeout": 5
}
]
}
Step 3: Test
# Should BLOCK (wrong weekday):
echo '{"tool_name":"Write","tool_input":{"file_path":"t.md","content":"Meeting June 9 (Mon)"}}' \
| node ~/.claude/hooks/date-weekday-verifier.js
# Should PASS (correct weekday):
echo '{"tool_name":"Write","tool_input":{"file_path":"t.md","content":"Meeting June 9 (Tue)"}}' \
| node ~/.claude/hooks/date-weekday-verifier.js
What It Looks Like in Action
When Claude tries to write a wrong weekday:
DATE VERIFIER: 1 wrong weekday(s)!
WRONG: "June 9 (Mon)" -- claimed Mon, actually Tue
Fix before proceeding.
The tool call is blocked. Claude automatically corrects the weekday and retries.
A fun side effect: while writing this article, the hook detected the intentional wrong-date examples in the code blocks and blocked the file write. I had to bypass my own trap.
Before / After
| Before (no Hook) | After (with Hook) | |
|---|---|---|
| Weekday accuracy | Dice roll (1/7) | 100% correct |
| Discovery | Pointed out by the other party | Blocked before writing |
| Fix cost | Embarrassing correction email | Automatic |
| Languages | — | English + Japanese |
Beyond Weekdays
This pattern extends far beyond date verification. By swapping the regex, you can validate:
- Phone number formats
- Email address validity
- Currency amounts and decimal places
- Address formatting
Anything Claude should verify before writing can be enforced with a Hook.
GitHub Issue with full code: anthropics/claude-code #63098
Top comments (0)