Claude Timestamp
Every message stamped with the time it happened, and how long it took.
A long session reads back as one wall of text. A timestamp gives each turn a place, and a duration gives it weight.
- Bash
- Claude Code
- hooks
- jq
GitHub (opens in a new tab)README (opens in a new tab)Changelog (opens in a new tab)
What it does
Claude Timestamp is a Claude Code plugin, not an MCP server: hooks that run at fixed points in a session, plus a /timestamps command to change them. Every assistant message gets a marker like [13:22:13] in your own timezone, followed by how long the turn took, such as +2m14s, counted from the moment you pressed enter to the moment Claude was done.
A turn that crosses a threshold you set gets its duration coloured, so you notice it instead of reading past it. With TOOL_TIMING on, the marker also names what made the turn slow: [13:22:13 +2m14s · Bash 1m58s]. A gap between two messages gets its own label, so a session you return to the next morning still reads in order, and the first message after midnight carries the date.
The model is also told the local time of each prompt, so it can reason about when things happened; you can switch that off and keep only the visible marker. On exit, a summary follows:
claude-timestamp: session lasted 1h30m over 12 turns, 24m18s of it waiting, 35m00s away.
slowest tools: Bash 41.2s (18 calls), WebFetch 8.1s (1 call), Read 2.0s (37 calls). 2 failedDisplay is display only. The marker is drawn as messages render, never enters the transcript, and never reaches the model.
Quick start
Requires jq and bash, nothing else. If jq is missing, the plugin says so once and then does nothing, rather than failing quietly.
claude plugin marketplace add https://aranea-development.nl/plugins/marketplace.json
claude plugin install claude-timestamp@araneaHooks are bound when a session starts, so start a new session before markers appear. An already-running session will not pick the plugin up.
There is nothing to set up. The defaults work as soon as it is installed, and the plugin points you at where to change them on the first run.
If the install stops at ssh: connect to host github.com port 22, git on that machine cannot reach GitHub over SSH, and the plugin is cloned from its GitHub repository. The message points at access rights, but the repository is public and the transport is what failed. One line fixes it, then install again:
git config --global --add url."https://github.com/".insteadOf "git@github.com:"The README has the longer version, including how to undo it.
Examples
A real Claude Code session played back at real speed, recorded with the project's own screenshot tooling.

Two fast turns render dim. The third crossed the slow threshold, so its duration is coloured and, with TOOL_TIMING on, named after the tool behind it.
Configuring it
/timestamps shows what you have now and offers a handful of presets, each previewed as the marker it actually produces.

The command also takes a request directly, so /timestamps tokyo, /timestamps no colour and /timestamps 12 hour clock each land in one step. Changes take effect on your next message, because every hook reads the config file each time it runs. Only installing needs a new session, since that is when hooks are bound.
Nothing about this runs a shell script. /timestamps reads schema.json, which ships with the plugin and describes every setting, and edits your config file directly.
What the marker looks like
MARKER decides what the marker is made of and how it is arranged. The parts are %time, %elapsed, %tool and %date, and a {...} group disappears when every part inside it is empty, so a turn with no duration is not left holding an empty pair of brackets:
MARKER= renders as
[{%date }%time{ %elapsed}{ · %tool}] [13:22:13 +2m14s · Bash 1m58s]
%time 13:22:13
%time{ %elapsed} 13:22:13 +2m14s
%time{ → %elapsed} 13:22:13 → +2m14sWriting a template yourself is optional. /timestamps knows a handful of shapes by name and previews what each one produces, and it takes a request just as readily: /timestamps no brackets and /timestamps just the clock each set the matching MARKER.

Each part can take its own colour, through TIME_COLOR, ELAPSED_COLOR and TOOL_COLOR. Leave one empty and that part follows COLOR. A slow turn still uses SLOW_COLOR: it wins over ELAPSED_COLOR once the turn crosses SLOW_AFTER, because standing out is the whole point of it.
From a terminal
If you would rather answer the questions yourself, the setup script has an interactive wizard. It needs a real TTY, so run it in a terminal rather than asking Claude to.
bash "$CLAUDE_PLUGIN_ROOT/hooks/scripts/setup.sh"
Every question shows its current value in brackets, and pressing enter keeps it. The colour list and the result line are rendered by the same code that draws the real marker, so a preview cannot drift from what you will actually see.
It also takes flags, so several settings can be set in one call:
setup.sh --tz=Asia/Tokyo --display=short --color=dim --slow-after=30Every flag is optional and anything you leave out keeps its current value.
Project settings
A project can carry its own settings in .claude/claude-timestamp.conf, layered over yours. Only the keys it names are overridden, so a repository can pin one thing and leave the rest following your own configuration:
cd some-project
bash "$CLAUDE_PLUGIN_ROOT/hooks/scripts/setup.sh" --project --tz=UTCThat writes only TZ=UTC. Everything else still comes from your account. The file is found by walking up from the directory the conversation is about, so it applies from subdirectories too, and the search stops at your home directory so your own config is never mistaken for a project one.
Where the time goes
Ask Claude how long you have been at this, or how much of it was waiting, and it reads the totals straight out of ~/.claude/claude-timestamp-history.tsv and answers in the chat. No command needed.
For a terminal view, run the script instead:
bash "$CLAUDE_PLUGIN_ROOT/hooks/scripts/setup.sh" --stats
Each finished session is appended to the history file, and the oldest are dropped once there are more than HISTORY_LIMIT of them. The file holds timings only: five numbers and a date per session. No message text, no tool arguments, and no paths, so nothing in it says what you were working on. Switch it off entirely with HISTORY=off.
When something is wrong
Ask Claude why you are not seeing timestamps and it reads the facts file and your config against schema.json to tell you what it finds. The most common cause is ENABLED=off, easy to set and forget since it silences every hook without a trace on screen.
For a terminal check, run doctor instead:
bash "$CLAUDE_PLUGIN_ROOT/hooks/scripts/setup.sh" --doctor
It checks that jq is present, that the config parses, that a pinned timezone can actually be applied on this machine, and that the state directory is writable, and exits non-zero if any of that fails. It also reports whether ENABLED is on: switching the plugin off on purpose is not itself a problem, so that line alone will not fail the check, but it is usually why you ran doctor in the first place.
How it works
Eight events, served by six scripts. One of them talks to the model: UserPromptSubmit tells Claude the local time of your message, one line, and INJECT_CONTEXT=off turns it off. Everything else stays in the harness and costs you no context.
| Hook | Job |
|---|---|
SessionStart | Check jq, prune old state, point a new user at /timestamps |
UserPromptSubmit | Record the turn start, tell the model the local time |
MessageDisplay | Draw the marker on the first batch of each message |
Stop / StopFailure | Close the turn at the moment it is actually finished |
SessionEnd | Report the summary, record the session, clear its state |
PostToolUse / PostToolUseFailure | Record what each tool call cost and count failures, only when TOOL_TIMING=on |
A turn's duration comes from Stop rather than from a guess about which messages went past. That is the difference between "the last message has been drawn" and "Claude is done".
MessageDisplay fires repeatedly as a message streams. Only the first batch is stamped; the rest return nothing at all, which Claude Code reads as "show the original text". Returning the text unchanged would have meant a wasted round trip on every batch of every message.
Timing state lives in $TMPDIR/claude-timestamp-<your uid>, one small file per session, cleared when the session ends and pruned after seven days. The uid is in the name because $TMPDIR is shared ground on a multi-user machine: the directory is created private to you, and one belonging to somebody else is declined rather than written into.
Configuration
Settings live in ~/.claude/claude-timestamp.conf as KEY=value, parsed against a list of known keys and never executed, so a stray line in it cannot run anything.
| Setting | Default | What it does |
|---|---|---|
ENABLED | on | Master switch; off silences every hook without uninstalling it |
TZ | machine local | IANA name such as Europe/Amsterdam, or empty for local time |
DISPLAY_FORMAT | 24h | 24h, short, 12h, iso, or any strftime string |
CONTEXT_FORMAT | 24h | The same values, for the time the model is told |
COLOR | dim | none, dim, gray, red, green, yellow, blue, magenta, cyan |
MARKER | [{%date }%time{ %elapsed}{ · %tool}] | The layout, built from %time, %elapsed, %tool and %date |
TIME_COLOR | empty | Colour of %time and %date; empty follows COLOR |
ELAPSED_COLOR | empty | Colour of %elapsed; SLOW_COLOR wins on a slow turn |
TOOL_COLOR | empty | Colour of %tool; empty follows COLOR |
ELAPSED | on | Show how long the turn took |
INJECT_CONTEXT | true | Tell the model the local time each prompt was sent |
SLOW_AFTER | 60 | Colour the duration past this many seconds, 0 disables |
SLOW_COLOR | yellow | Colour used for a slow turn |
IDLE_AFTER | 3600 | Mark a gap this long between messages, 0 disables |
DATE_ROLLOVER | on | Show the date on the first message after midnight |
SUMMARY | on | Report session totals on exit |
SUBAGENTS | on | Stamp subagent messages as well |
TOOL_TIMING | off | Time individual tool calls and name the slowest |
HISTORY | on | Record each finished session, for /timestamps and --stats |
HISTORY_LIMIT | 200 | How many recorded sessions to keep |
NO_COLOR disables colour whatever COLOR says.
A value the plugin cannot use is replaced by its default rather than silently doing nothing, and it is named at the start of the next session and by --doctor.
Clock formats render as 14:03:22 for 24h, 14:03 for short, 2:03 PM for 12h, and 2026-08-19T14:03:22 for iso. Any value containing a % is treated as a strftime string, so the escape hatch needs no separate setting.
TOOL_TIMING is off by default because it is the only setting that costs anything per tool call. Everything else costs once per message.
Alongside the config, the plugin writes ~/.claude/claude-timestamp.facts.json at the start of every session. It holds what cannot be worked out by reading the configuration: whether this machine has a timezone database, whether the state directory is writable, and which version is installed. That is what lets /timestamps answer questions about your setup without running anything.
Platform notes
Tested on Linux, macOS and Windows on every push. Git Bash on Windows ships without a timezone database: date there silently falls back to UTC for any IANA name it cannot resolve. The plugin detects this, uses local time instead, mentions it once at session start, and refuses to write a pinned zone it knows cannot be honoured. UTC and GMT still work, since those need no database.
The plugin does not time tool calls itself. Claude Code reports how long each one took and the hook only reads that number off the payload. TOOL_TIMING is still off by default, because it is the one setting that costs something per tool call rather than once per message: the hook that records the number runs on every call.