{"slug": "give-llm-agents-claude-in-this-case-but-may-be-adaptable-to-others-an-awareness", "title": "Give LLM agents (Claude in this case, but may be adaptable to others) an awareness of time in an unobtrusive way.", "summary": "A developer created a time-awareness hook for LLM agents like Claude Code that injects timestamps at a human-like cadence, keeping the agent oriented in time without filling the context window with noise. The hook is rate-limited to emit a full timestamp once per hour and a short timestamp when more than five minutes have passed, using a pure function with no I/O for testability. It works with Claude Code 2.1.177 and can be wired into UserPromptSubmit and PostToolUse events via a single script.", "body_md": "A self-contained recipe. Read this one file and you can reproduce the whole mechanism from scratch — a human or another LLM, knowing nothing else.\n\nAn LLM agent has **no clock**. It only knows what is in its context window.\nSo:\n\n- Inject a timestamp on\n*every*action → the context fills with noise. - Inject one\n*once*at session start → it goes stale within minutes.\n\nThe goal is to inject timestamps at a **human-like cadence**: frequent enough\nthat the agent stays oriented in time (and notices the *passage* of time — e.g.\n\"that build took 40 minutes\"), but sparse enough to be almost invisible.\n\nClaude Code **hooks** are external commands the harness runs on certain events.\nA hook can print a line of JSON of this shape:\n\n```\n{\"hookSpecificOutput\":{\"hookEventName\":\"<event>\",\"additionalContext\":\"⏱ ...\"}}\n```\n\nThe `additionalContext`\n\nstring is **injected into the model's context**. That is\nthe entire delivery channel: the hook prints that line, the model sees the\ntimestamp on its next think.\n\n- Verified working on\n**Claude Code 2.1.177**. - Every hook receives a JSON object on\n**stdin** that includes`session_id`\n\nand`hook_event_name`\n\n(plus event-specific fields). `additionalContext`\n\ninjection works for both`UserPromptSubmit`\n\nand`PostToolUse`\n\nevents (the two we use).\n\nWire the **same** script into two events:\n\n— fires once per user turn → tracks the`UserPromptSubmit`\n\n**human's** cadence (a fresh stamp each time you talk to the agent).— fires after each tool call → tracks time during long`PostToolUse`\n\n**autonomous** runs when the human isn't typing.\n\nBoth call one script which is internally **rate-limited**, so the vast majority\nof `PostToolUse`\n\ninvocations print *nothing*. That is what makes it cheap to\nleave enabled on every tool call.\n\nThe script emits one of three things:\n\n**long**—`⏱ Monday, June 23, 2026 10:27 PM EDT`\n\n— the**first** emit of each wall-clock hour.**short**—`⏱ 10:33 PM EDT`\n\n— when**more than 5 minutes** have passed since the last short emit.**none**— silent, otherwise.\n\nA **long emit also resets the short timer**, so you never get a long immediately\nechoed by a short.\n\nThe decision is a pure function of four numbers, with **no I/O** — which makes it\ntrivially testable:\n\n```\ndecide(now_epoch, now_hourkey, last_hourkey, last_short_epoch):\n  if   now_hourkey != last_hourkey        -> long   (new state: now_hourkey, now_epoch)   # incl. first run\n  elif (now_epoch - last_short) > 300      -> short  (new state: last_hourkey, now_epoch)\n  else                                     -> none   (state unchanged)\n```\n\n`hourkey`\n\nis `date +%Y%m%d%H`\n\n— so a new hour, a new day, or \"24h later, same\nhour\" all register as a new hour.\n\n| Part | Path |\n|---|---|\n| The script (executable) | `~/bin/time-awareness-hook` (here: a symlink into `~/dotfiles/bin/` ) |\n| The tests | `~/dotfiles/bin/test/time-awareness-hook_test` |\n| The wiring | `~/.claude/settings.json` → `hooks` |\n| Per-session state | `${XDG_STATE_HOME:-~/.local/state}/claude-time-awareness/state-<session_id>` |\n\nState is **per session** (keyed on the hook's `session_id`\n\n), so concurrent\nsessions don't interfere; the state file is two lines (`hourkey`\n\nthen\n`last_short_epoch`\n\n). Stale state files (>1 day) are pruned on each long emit.\n\nSave the following as an executable on your `PATH`\n\n(e.g. `~/bin/time-awareness-hook`\n\n),\nthen `chmod +x`\n\nit.\n\n``` bash\n#!/usr/bin/env bash\n# time-awareness-hook — emit a rate-limited human timestamp so Claude Code stays\n# aware of wall-clock time and its passage (it has no clock; it only knows what\n# lands in context). Wired into ~/.claude/settings.json as a UserPromptSubmit\n# and/or PostToolUse hook; the emitted JSON's additionalContext is injected into\n# the model's context.\n#\n# Cadence (mimics how a human glances at a clock): emit the LONG form (weekday +\n# full date + time) the first time per wall-clock hour, the SHORT form (just the\n# time) when MORE than 5 minutes have passed since the last short, and nothing\n# otherwise. A LONG emit also resets the short timer so you never get a long\n# immediately echoed by a short.\n#\n# Architecture is hexagonal: a PURE decision core (`--decide`, no I/O) decides\n# none|short|long from (now_epoch, now_hourkey, last_hourkey, last_short_epoch);\n# the adapter supplies those from the clock + a per-session state file and emits.\n# Test seams (env): TIMESTAMP_HOOK_NOW (epoch override), TIMESTAMP_HOOK_STATE_DIR,\n# TIMESTAMP_HOOK_SESSION. State lives under XDG_STATE_HOME, one file per session.\nset -u\n\nSHORT_INTERVAL=300   # seconds; \"more than 5 minutes\" => strict > this\n\n# --- pure core -------------------------------------------------------------\n# decide NOW_EPOCH NOW_HOURKEY LAST_HOURKEY LAST_SHORT_EPOCH\n# prints: \"<emit> <new_hourkey> <new_short_epoch>\"  (emit in none|short|long)\n_decide() {\n\tlocal now_epoch=\"$1\" now_hourkey=\"$2\" last_hourkey=\"$3\" last_short=\"${4:-0}\"\n\tif [ \"$now_hourkey\" != \"$last_hourkey\" ]; then\n\t\tprintf 'long %s %s\\n' \"$now_hourkey\" \"$now_epoch\"          # new hour (incl. first run)\n\telif [ $(( now_epoch - last_short )) -gt \"$SHORT_INTERVAL\" ]; then\n\t\tprintf 'short %s %s\\n' \"$last_hourkey\" \"$now_epoch\"        # >5 min since last short\n\telse\n\t\tprintf 'none %s %s\\n' \"$last_hourkey\" \"$last_short\"        # too soon; state unchanged\n\tfi\n}\n\n# --- adapter helpers -------------------------------------------------------\n# portable epoch -> formatted string (GNU date first, BSD date fallback)\n_fmt() { date -d \"@$1\" +\"$2\" 2>/dev/null || date -r \"$1\" +\"$2\"; }\n\n# pull a top-level string field out of the hook's stdin JSON\n_json_get() {\n\tlocal json=\"$1\" key=\"$2\"\n\tif command -v jq >/dev/null 2>&1; then\n\t\tprintf '%s' \"$json\" | jq -r --arg k \"$key\" '.[$k] // empty' 2>/dev/null\n\telse\n\t\tprintf '%s' \"$json\" | grep -oE \"\\\"$key\\\"[[:space:]]*:[[:space:]]*\\\"[^\\\"]*\\\"\" \\\n\t\t\t| head -1 | sed -E \"s/.*:[[:space:]]*\\\"([^\\\"]*)\\\"/\\1/\"\n\tfi\n}\n\n_safe() { printf '%s' \"$1\" | tr -c 'A-Za-z0-9_.-' '_'; }\n\n_usage() {\n\tcat <<'EOF'\ntime-awareness-hook — rate-limited timestamp injection for Claude Code\n\nUSAGE\n  time-awareness-hook                 # hook mode: reads hook JSON on stdin, emits\n                                      # additionalContext JSON (or nothing) on stdout\n  time-awareness-hook --decide NOW HOURKEY LAST_HOURKEY LAST_SHORT\n                                      # pure core: prints \"<none|short|long> <hourkey> <short_epoch>\"\n  time-awareness-hook -h | --help\n  time-awareness-hook --about\n  time-awareness-hook --test          # run test suite (stdout muted; exit = #fails)\n\nCADENCE\n  long  : first emit of each wall-clock hour (weekday, full date, time)\n  short : when > 5 min since the last short (just the time)\n  none  : otherwise (silent)\n  A long emit resets the short timer.\n\nENV (test seams)\n  TIMESTAMP_HOOK_NOW         epoch-seconds override for \"now\"\n  TIMESTAMP_HOOK_STATE_DIR   state directory (default: $XDG_STATE_HOME/claude-time-awareness)\n  TIMESTAMP_HOOK_SESSION     session id fallback when stdin lacks one\nEOF\n}\n\n# --- hook mode -------------------------------------------------------------\n_hook_main() {\n\t# never block waiting on a tty when run by hand with no stdin\n\tlocal input=\"\"\n\tif [ ! -t 0 ]; then input=\"$(cat)\"; fi\n\n\tlocal session event\n\tsession=\"$(_json_get \"$input\" session_id)\"\n\tevent=\"$(_json_get \"$input\" hook_event_name)\"\n\t[ -n \"$session\" ] || session=\"${TIMESTAMP_HOOK_SESSION:-default}\"\n\t[ -n \"$event\" ] || event=\"UserPromptSubmit\"\n\n\tlocal now state_dir state_file\n\tnow=\"${TIMESTAMP_HOOK_NOW:-$(date +%s)}\"\n\tstate_dir=\"${TIMESTAMP_HOOK_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/claude-time-awareness}\"\n\tmkdir -p \"$state_dir\" 2>/dev/null || true\n\tstate_file=\"$state_dir/state-$(_safe \"$session\")\"\n\n\tlocal now_hourkey last_hourkey=\"\" last_short=\"0\"\n\tnow_hourkey=\"$(_fmt \"$now\" '%Y%m%d%H')\"\n\tif [ -f \"$state_file\" ]; then\n\t\t{ read -r last_hourkey; read -r last_short; } < \"$state_file\" 2>/dev/null || true\n\t\t[ -n \"$last_short\" ] || last_short=\"0\"\n\tfi\n\n\tlocal emit new_hourkey new_short\n\tread -r emit new_hourkey new_short < <(_decide \"$now\" \"$now_hourkey\" \"$last_hourkey\" \"$last_short\")\n\n\t[ \"$emit\" = \"none\" ] && return 0\n\n\t# persist new state atomically (only on an actual emit)\n\tlocal tmp=\"$state_file.tmp.$$\"\n\tprintf '%s\\n%s\\n' \"$new_hourkey\" \"$new_short\" > \"$tmp\" 2>/dev/null && mv -f \"$tmp\" \"$state_file\" 2>/dev/null\n\n\t# hourly housekeeping: drop state files older than a day (cheap, runs ~once/hour)\n\t[ \"$emit\" = \"long\" ] && find \"$state_dir\" -maxdepth 1 -type f -name 'state-*' -mtime +0 -delete 2>/dev/null\n\n\tlocal stamp\n\tcase \"$emit\" in\n\t\tlong)  stamp=\"$(_fmt \"$now\" '⏱ %A, %B %d, %Y %I:%M %p %Z')\" ;;\n\t\tshort) stamp=\"$(_fmt \"$now\" '⏱ %I:%M %p %Z')\" ;;\n\tesac\n\tprintf '{\"hookSpecificOutput\":{\"hookEventName\":\"%s\",\"additionalContext\":\"%s\"}}\\n' \"$event\" \"$stamp\"\n}\n\n# --- dispatch --------------------------------------------------------------\ncase \"${1:-}\" in\n\t-h|--help)   _usage; exit 0 ;;\n\t--about)     echo \"time-awareness-hook: rate-limited timestamp injection for Claude Code ($(uname -s) $(uname -m))\"; exit 0 ;;\n\t--decide)    shift; _decide \"$@\"; exit 0 ;;\n\t--test)\n\t\t_tf=\"$HOME/dotfiles/bin/test/time-awareness-hook_test\"\n\t\tif [ -f \"$_tf\" ]; then exec bash \"$_tf\" >/dev/null; fi\n\t\techo \"no test file at $_tf\" >&2; exit 0 ;;\n\t*)           _hook_main; exit 0 ;;\nesac\n```\n\nNote: the bash function bodies are\n\ntab-indented. If you copy/paste, keep the tabs (or it still runs — bash doesn't care — but match your house style).\n\nEdit `~/.claude/settings.json`\n\n(user-global) — or a project's\n`.claude/settings.json`\n\n— and **merge** this `hooks`\n\nobject in. If a `hooks`\n\nobject already exists, add these two keys to it; do **not** clobber sibling hooks.\n\n```\n{\n  \"hooks\": {\n    \"UserPromptSubmit\": [\n      { \"hooks\": [ { \"type\": \"command\", \"command\": \"$HOME/bin/time-awareness-hook\" } ] }\n    ],\n    \"PostToolUse\": [\n      { \"hooks\": [ { \"type\": \"command\", \"command\": \"$HOME/bin/time-awareness-hook\" } ] }\n    ]\n  }\n}\n```\n\n- Omitting\n`\"matcher\"`\n\non`PostToolUse`\n\nmatches**all** tools (fine — it's rate-limited). Add`\"matcher\": \"Bash\"`\n\n(etc.) to scope it to specific tools. - Use\n`$HOME/bin/...`\n\n(not a bare name) for portability across machines. **The effect is immediate — no session restart needed.**\n\n```\n# Long form (first call of the hour for a fresh session):\nprintf '{\"hook_event_name\":\"PostToolUse\",\"session_id\":\"demo\"}' \\\n  | TIMESTAMP_HOOK_STATE_DIR=\"$(mktemp -d)\" ~/bin/time-awareness-hook\n# -> {\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":\"⏱ Monday, June 23, 2026 10:27 PM EDT\"}}\n```\n\nIn a live session you'll then see lines like `⏱ 10:33 PM EDT`\n\nappear in the\nagent's context — long once per hour, short after >5 min, silent otherwise.\n\nSave as `~/dotfiles/bin/test/time-awareness-hook_test`\n\nand run it directly\n(`bash time-awareness-hook_test`\n\n) — it exits with the number of failures. It\ntests the pure core at every boundary (first run, the exactly-300s vs 301s line,\nthe hour-rollover override) and the adapter with an **injected clock** and an\nisolated temp state dir, so it's fully deterministic.\n\n``` bash\n#!/usr/bin/env bash\n# Tests for time-awareness-hook: rate-limited timestamp injection for Claude time cognizance.\n#\n# Architecture under test is hexagonal: a PURE decision core (`--decide`) that takes\n# (now_epoch, now_hourkey, last_hourkey, last_short_epoch) and emits one of none|short|long\n# plus the new state — no clock, no state file, no stdin. The hook adapter (default mode)\n# reads stdin JSON + clock + state and calls that same core. We test the pure core directly\n# (deterministic, no I/O) and exercise the adapter with an injected clock + temp state dir.\n#\n# Conventions: progress -> stdout, failures -> stderr, exit code == number of failures.\n# NEVER use set -e here (error paths are under test); set -u only.\nset -u\n\nHOOK=\"$HOME/dotfiles/bin/time-awareness-hook\"\ntests=0\nfails=0\n\nexpect_eq() { # actual expected msg\n\t((tests++))\n\tif [ \"$1\" = \"$2\" ]; then\n\t\techo \"✓ $3\"\n\telse\n\t\techo \"✗ $3\" >&2\n\t\techo \"    expected: [$2]\" >&2\n\t\techo \"    actual:   [$1]\" >&2\n\t\t((fails++))\n\tfi\n}\nexpect_contains() { # haystack needle msg\n\t((tests++))\n\tif printf '%s' \"$1\" | grep -q -- \"$2\"; then echo \"✓ $3\"\n\telse echo \"✗ $3\" >&2; echo \"    [$1] lacks [$2]\" >&2; ((fails++)); fi\n}\nexpect_not_contains() { # haystack needle msg\n\t((tests++))\n\tif printf '%s' \"$1\" | grep -q -- \"$2\"; then echo \"✗ $3\" >&2; echo \"    [$1] unexpectedly has [$2]\" >&2; ((fails++))\n\telse echo \"✓ $3\"; fi\n}\n\n# portable epoch->format (GNU first, BSD fallback) so expectations match the hook\nfmt() { date -d \"@$1\" +\"$2\" 2>/dev/null || date -r \"$1\" +\"$2\"; }\n\n# ---- PURE CORE: time-awareness-hook --decide now_epoch now_hourkey last_hourkey last_short ----\nemit()  { \"$HOOK\" --decide \"$@\" | cut -d' ' -f1; }\nstate() { \"$HOOK\" --decide \"$@\" | cut -d' ' -f2-; }\n\necho \"# pure decision core\"\n# 1. first run (empty prior hour) -> long, short reset to now\nexpect_eq \"$(emit 1000000000 2026062221 '' 0)\" \"long\"                       \"first run -> long\"\nexpect_eq \"$(state 1000000000 2026062221 '' 0)\" \"2026062221 1000000000\"     \"first run state = nowhour + now\"\n# 2. same hour, 100s since short -> none\nexpect_eq \"$(emit 1000000100 2026062221 2026062221 1000000000)\" \"none\"      \"same hour <5min -> none\"\n# 3. same hour, exactly 300s -> none (rule is 'MORE than 5 min', strict >)\nexpect_eq \"$(emit 1000000300 2026062221 2026062221 1000000000)\" \"none\"      \"exactly 5min -> none (strict)\"\n# 4. same hour, 301s -> short, updates short epoch, keeps hour\nexpect_eq \"$(emit 1000000301 2026062221 2026062221 1000000000)\" \"short\"     \"just over 5min -> short\"\nexpect_eq \"$(state 1000000301 2026062221 2026062221 1000000000)\" \"2026062221 1000000301\" \"short keeps hour, bumps short epoch\"\n# 5. hour changed but short was 10s ago -> long overrides + resets short timer\nexpect_eq \"$(emit 1000000010 2026062222 2026062221 1000000000)\" \"long\"      \"hour change overrides recent short\"\nexpect_eq \"$(state 1000000010 2026062222 2026062221 1000000000)\" \"2026062222 1000000010\" \"long resets short epoch to now\"\n\n# ---- ADAPTER: full stdin/state path with injected clock + isolated state dir ----\necho \"# hook adapter (injected clock + temp state)\"\n# --tmpdir places this in $TMPDIR (RAM); per project convention it self-cleans,\n# and avoiding an explicit rm dodges the rm-safe wrapper's noise on teardown.\nTMP=\"$(mktemp -d --tmpdir time-awareness-hook.XXXXXX)\"\nrun() { # now_epoch session\n\tprintf '{\"hook_event_name\":\"PostToolUse\",\"session_id\":\"%s\"}' \"$2\" \\\n\t\t| TIMESTAMP_HOOK_NOW=\"$1\" TIMESTAMP_HOOK_STATE_DIR=\"$TMP\" \"$HOOK\"\n}\nWD=\"$(fmt 1000000000 %A)\"   # weekday name only appears in the LONG format\n\n# 6. fresh session -> long emit (JSON additionalContext, weekday present) + state file created\no6=\"$(run 1000000000 sessA)\"\nexpect_contains     \"$o6\" \"additionalContext\" \"fresh session emits additionalContext JSON\"\nexpect_contains     \"$o6\" \"$WD\"               \"fresh session emit is LONG (weekday present)\"\nexpect_eq \"$([ -f \"$TMP/state-sessA\" ] && echo yes)\" \"yes\" \"state file created for session\"\n# 7. same session 2 min later, same hour -> none (silent)\nexpect_eq \"$(run 1000000120 sessA)\" \"\" \"2 min later same hour -> no output\"\n# 8. same session ~6.5 min later -> short (time present, weekday absent)\no8=\"$(run 1000000400 sessA)\"\nexpect_contains     \"$o8\" \"additionalContext\" \"6+ min later emits\"\nexpect_not_contains \"$o8\" \"$WD\"               \"6+ min emit is SHORT (no weekday)\"\n# 9. session isolation: a different session id is fresh -> long, separate state file\no9=\"$(run 1000000400 sessB)\"\nexpect_contains     \"$o9\" \"$WD\"               \"different session is independent -> LONG\"\nexpect_eq \"$([ -f \"$TMP/state-sessB\" ] && echo yes)\" \"yes\" \"separate state file per session\"\n\n# ---- summary ----\nif [ \"$fails\" -gt 0 ]; then\n\techo \"$fails of $tests tests FAILED\" >&2\nelse\n\techo \"all $tests tests passed\"\nfi\nexit \"$fails\"\n```\n\n**Bash**+(GNU`date`\n\n`date -d @epoch`\n\nis tried first, BSD`date -r epoch`\n\nis the fallback — so it works on both Linux and macOS).is used if present; otherwise a`jq`\n\n`grep`\n\n/`sed`\n\nfallback parses the two fields we need from the stdin JSON. So**jq is optional**.- A\n**Claude Code** recent enough to inject`hookSpecificOutput.additionalContext`\n\nfor`UserPromptSubmit`\n\n/`PostToolUse`\n\n(confirmed on**2.1.177**).\n\n**Two hooks** because the human and the agent move on different clocks: a user turn is the human's heartbeat; a tool call is the agent's. Covering both means the agent is oriented whether you're driving or it's running autonomously.**Rate-limiting in the script, not the wiring**, so`PostToolUse`\n\ncan stay on*every*tool with negligible cost — it returns instantly with no output the vast majority of the time.**Long-hourly / short->5min / silent** mirrors how people actually track time: you register the hour when it rolls over, and you're vaguely aware of ~5-minute chunks in between. The long-emit-resets-short rule prevents a redundant short right after a long.**The pure**(no clock, no files, no stdin) is the part worth lifting into any agent harness — it's what makes the cadence testable with an injected clock, and it's language-agnostic (port the 3-line rule anywhere).`--decide`\n\ncore**Per-session state** keyed on`session_id`\n\nkeeps concurrent agent sessions from stomping each other's timers.", "url": "https://wpnews.pro/news/give-llm-agents-claude-in-this-case-but-may-be-adaptable-to-others-an-awareness", "canonical_source": "https://gist.github.com/pmarreck/a36afbf9b00a67de510191ad1cf521f7", "published_at": "2026-06-24 02:23:56+00:00", "updated_at": "2026-06-26 21:34:22.037609+00:00", "lang": "en", "topics": ["large-language-models", "ai-agents", "developer-tools"], "entities": ["Claude Code", "Claude", "Anthropic"], "alternates": {"html": "https://wpnews.pro/news/give-llm-agents-claude-in-this-case-but-may-be-adaptable-to-others-an-awareness", "markdown": "https://wpnews.pro/news/give-llm-agents-claude-in-this-case-but-may-be-adaptable-to-others-an-awareness.md", "text": "https://wpnews.pro/news/give-llm-agents-claude-in-this-case-but-may-be-adaptable-to-others-an-awareness.txt", "jsonld": "https://wpnews.pro/news/give-llm-agents-claude-in-this-case-but-may-be-adaptable-to-others-an-awareness.jsonld"}}