Skip to main content

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.

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:

text
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 failed

Display 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.

bash
claude plugin marketplace add https://aranea-development.nl/plugins/marketplace.json
claude plugin install claude-timestamp@aranea

Hooks 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:

bash
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.

Timestamps on assistant messages, with a slow turn highlighted and the tool that caused it named

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 in-chat command, showing presets with a preview of each marker

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:

text
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 → +2m14s

Writing 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.

The shapes the plugin knows by name, each with the marker it produces

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
bash "$CLAUDE_PLUGIN_ROOT/hooks/scripts/setup.sh"

The setup wizard, showing the colour choices and a live preview

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:

bash
setup.sh --tz=Asia/Tokyo --display=short --color=dim --slow-after=30

Every 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:

bash
cd some-project
bash "$CLAUDE_PLUGIN_ROOT/hooks/scripts/setup.sh" --project --tz=UTC

That 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
bash "$CLAUDE_PLUGIN_ROOT/hooks/scripts/setup.sh" --stats

Totals across recorded sessions

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
bash "$CLAUDE_PLUGIN_ROOT/hooks/scripts/setup.sh" --doctor

Output of the doctor self-check

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.

HookJob
SessionStartCheck jq, prune old state, point a new user at /timestamps
UserPromptSubmitRecord the turn start, tell the model the local time
MessageDisplayDraw the marker on the first batch of each message
Stop / StopFailureClose the turn at the moment it is actually finished
SessionEndReport the summary, record the session, clear its state
PostToolUse / PostToolUseFailureRecord 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.

SettingDefaultWhat it does
ENABLEDonMaster switch; off silences every hook without uninstalling it
TZmachine localIANA name such as Europe/Amsterdam, or empty for local time
DISPLAY_FORMAT24h24h, short, 12h, iso, or any strftime string
CONTEXT_FORMAT24hThe same values, for the time the model is told
COLORdimnone, dim, gray, red, green, yellow, blue, magenta, cyan
MARKER[{%date }%time{ %elapsed}{ · %tool}]The layout, built from %time, %elapsed, %tool and %date
TIME_COLORemptyColour of %time and %date; empty follows COLOR
ELAPSED_COLORemptyColour of %elapsed; SLOW_COLOR wins on a slow turn
TOOL_COLORemptyColour of %tool; empty follows COLOR
ELAPSEDonShow how long the turn took
INJECT_CONTEXTtrueTell the model the local time each prompt was sent
SLOW_AFTER60Colour the duration past this many seconds, 0 disables
SLOW_COLORyellowColour used for a slow turn
IDLE_AFTER3600Mark a gap this long between messages, 0 disables
DATE_ROLLOVERonShow the date on the first message after midnight
SUMMARYonReport session totals on exit
SUBAGENTSonStamp subagent messages as well
TOOL_TIMINGoffTime individual tool calls and name the slowest
HISTORYonRecord each finished session, for /timestamps and --stats
HISTORY_LIMIT200How 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.