See the time on every message in Claude Code — and let Claude know it too.
Illustration of the plugin in action — each reply is prefixed with the local time, and Claude can reason about elapsed time.
[09:12:54] Here's the plan: I'll start by …
Claude Code doesn't show when anything happened. Long sessions blur together: you can't tell how long a build took, when you asked something, or point a teammate to "the part around 09:15." This plugin fixes that in two ways, and installs in 15 seconds.
Timestamps are one of the most-requested missing features in Claude Code. At least 11 issues ask for some form of it:
#2441 · #60711 · #44763 · #30745 · #21051 · #63488 · #61672 · #52944 · #56855 · #49084 · #60492
This plugin is a working solution today, while we wait to see if it ships natively.
| Without it | With it |
|---|---|
| "How long did that step take?" — no idea | Read it off the timestamps |
| "When did I ask that?" — scroll and guess | It's right there on the message |
| "Look at the part around 9am" — good luck | Every message is time-labeled |
| Claude has no sense of elapsed time | Claude knows when each prompt was sent and can reason about it |
No tracking, no telemetry, no account, no network calls. It just reads your computer's clock. Runs in a few milliseconds per message.
In Claude Code, run these two lines:
/plugin marketplace add zoharbabin/claude-code-message-timestamps
/plugin install message-timestamps@zoharbabin-claude-tools
Restart Claude Code. That's it — timestamps now appear on every message, in every project. ✅
Prefer the terminal?
claude plugin marketplace add zoharbabin/claude-code-message-timestampsthenclaude plugin install message-timestamps@zoharbabin-claude-tools.
To remove it: /plugin uninstall message-timestamps@zoharbabin-claude-tools.
- Claude Code 2.1.152 or newer — that version added the
MessageDisplayhook used for the on-screen timestamps. (Older versions still get the "Claude knows the time" half.) jqon yourPATH:- macOS:
brew install jq - Linux:
sudo apt-get install jq/sudo dnf install jq - Windows:
winget install jqlang.jqorchoco install jq
- macOS:
- Windows only: the hooks are bash scripts, so you need Git Bash (bundled with
Git for Windows). Claude Code runs
commandhooks via Git Bash automatically when it's installed.
If jq is missing the plugin fails safe — it adds no timestamp rather than breaking
anything, so you never lose a message. It also shows a one-time message at session start
telling you exactly how to install jq. Install it, restart, and timestamps light up.
The hooks are small Bash scripts that call date (for local time) and jq (to build
JSON safely). Claude Code runs command hooks through a shell: sh -c on macOS/Linux,
and Git Bash on Windows (falling back to PowerShell if Git Bash isn't installed).
Here's exactly how that plays out:
| macOS | Linux | Windows | |
|---|---|---|---|
| Shell used for hooks | sh / system bash |
sh / system bash |
Git Bash (auto-detected) |
date (local time) |
built in | built in | provided by Git Bash |
jq |
brew install jq |
apt/dnf install jq |
winget/choco install jq |
On-screen [HH:MM:SS] |
✅ (Claude Code 2.1.152+) | ✅ (2.1.152+) | ✅ (2.1.152+, with Git Bash) |
| Claude knows the time | ✅ | ✅ | ✅ (with Git Bash) |
| If Git Bash is absent | n/a | n/a | scripts can't run — install Git Bash |
If jq is absent |
one-time notice, no timestamps | one-time notice, no timestamps | one-time notice, no timestamps |
Why it works without fuss:
- No executable-bit dependency. The hooks are invoked as
bash <script>, so they run even when a clone, a ZIP download, orgit config core.fileMode=falsedrops the+xpermission — common on Windows. - Windows path safety. On Windows,
CLAUDE_PLUGIN_ROOTis a backslash path (C:\Users\…). The hooks use the shell form with the unbraced, double-quoted variable —bash "$CLAUDE_PLUGIN_ROOT/hooks/scripts/…sh". Claude Code substitutes the braced${CLAUDE_PLUGIN_ROOT}itself (and its backslashes get eaten while the command is assembled), but it leaves the unbraced$CLAUDE_PLUGIN_ROOTfor bash to expand at runtime, where the backslashes survive intact. This is also safer than substituting the path in: bash does not re-scan the result of a variable expansion, so a path containing shell metacharacters can't trigger command substitution. Inert on macOS/Linux and space-safe everywhere. - Local time, not UTC. Time comes from your shell's
date, so it matches your wall clock. (The scripts deliberately avoidjq'snow | strftime, which is UTC-only.) - Graceful, explained degradation. Missing
jqnever breaks a message or errors the session; you get a one-timeSessionStartnotice with the exact install command for your OS, and normal output continues untouched.
Windows, specifically: install Git for Windows (this
provides Git Bash, bash, and date) plus jq. Claude Code then routes the hooks
through Git Bash automatically — no extra configuration. Native cmd.exe/PowerShell
without Git Bash is not supported, because the hooks are Bash scripts.
WSL / dev containers / SSH remotes: these are Linux environments, so they behave exactly like the Linux column — just make sure
jqis installed inside that environment, not only on the host.
Each assistant reply is prefixed with the local time, once per message:
[09:12:54] Sure — here's how that works …
[09:13:48] Done. Tests pass.
The gap between two stamps tells you how long the work in between took.
Three small hooks, each on a different Claude Code event:
-
You see the time (
MessageDisplay) — prepends[HH:MM:SS]to each assistant message on screen only. It never touches the transcript or what Claude reads, so it can't confuse the model. It stamps just the first chunk of each streamed message, so you get exactly one timestamp per reply. -
Claude knows the time (
UserPromptSubmit) — addsMessage sent at local time 09:12:54 EDTto Claude's context when you send a prompt. Claude Code wraps this in a<system-reminder>, which the model is trained to treat as background metadata — not as text you typed. So Claude can answer "how long ago did I ask that?" without ever echoing or fixating on the clock. -
It tells you if something's missing (
SessionStart) — runs once when a session starts and, only ifjqisn't installed, shows a one-time notice with the install command for your OS. When everything's present it stays silent.
This split is deliberate: the visible marker stays out of the model's context (zero risk of confusion), and the model's time-awareness stays out of your view (zero visual clutter). Both halves use a tiny, fixed token footprint and your machine's local timezone.
claude-code-message-timestamps/
├── .claude-plugin/
│ ├── plugin.json # plugin manifest
│ └── marketplace.json # lets you install straight from this repo
├── hooks/
│ ├── hooks.json # wires up the three hooks
│ └── scripts/
│ ├── timestamp-context.sh # UserPromptSubmit → tells Claude the time
│ ├── timestamp-display.sh # MessageDisplay → shows you the time
│ ├── dependency-check.sh # SessionStart → warns once if jq is missing
│ └── lib/
│ └── resolve-tz.sh # maps CLAUDE_TIMESTAMPS_TZ → IANA zone
└── README.md
Set the CLAUDE_TIMESTAMPS_FORMAT environment variable to any standard
date format string. It
applies to both the on-screen marker and the time injected into Claude's context,
and defaults to %H:%M:%S. Set it once in your shell profile (.bashrc,
.zshrc, …) or in .claude/settings.json:
{ "env": { "CLAUDE_TIMESTAMPS_FORMAT": "%H:%M" } }- No seconds:
%H:%M - Add the date too:
%Y-%m-%d %H:%M:%S - 12-hour clock:
%I:%M %p
The model-facing context always appends the timezone (%Z) on top of this
format, so Claude keeps the offset. To change the wording around the time Claude
sees, edit the additionalContext string in timestamp-context.sh.
By default, timestamps use your machine's local timezone. Set
CLAUDE_TIMESTAMPS_TZ to render in a specific zone instead — handy if your
machine's clock is on one timezone but you want to reason about another:
CLAUDE_TIMESTAMPS_TZ=KST claude # -> Asia/Seoul
CLAUDE_TIMESTAMPS_TZ=PST claude # -> America/Los_Angeles
CLAUDE_TIMESTAMPS_TZ=Asia/Tokyo claude # any IANA zone name also worksCommon abbreviations (KST, JST, IST, PST, EST, CST, MST, GMT,
CET, AEST, …) are mapped to full IANA zones in
hooks/scripts/lib/resolve-tz.sh; anything else is passed straight to date
as-is, so any IANA name works too. It composes with CLAUDE_TIMESTAMPS_FORMAT
— include %Z in your format if you want the zone abbreviation to show.
- Display-only mode (suppress context injection): if you want the on-screen
[HH:MM:SS]marker but don't want the time injected into Claude's context, setCLAUDE_TIMESTAMPS_INJECT_CONTEXT=falsein your shell profile (.bashrc,.zshrc, etc.). TheMessageDisplaytimestamp is unaffected; only theUserPromptSubmitcontext is suppressed.
Note on timezones: the scripts use the shell's
datecommand, which respects your local timezone. They intentionally avoidjq'snow | strftime, which renders in UTC and would show the wrong time.
You can paste the same hooks into ~/.claude/settings.json by hand under a "hooks"
key. The plugin is just a cleaner, updatable, no-clobber way to do it — and it won't
overwrite hooks you already have.
Does this send my data anywhere? No. It reads your local clock and nothing else.
Will it slow Claude Code down? No — each hook runs in a few milliseconds.
Can I timestamp my own messages on screen too? Not currently — Claude Code's
MessageDisplay hook only renders assistant messages. Your send-times still reach
Claude via the other hook, so the elapsed-time math is intact.
Does it work in every project? Yes, once installed it's global across all projects and sessions.
MIT © Zohar Babin
