---
title: "The Instruction That Reached the Model on Zero Prompts"
canonical: https://dxdev.com/blog/footer-instruction-reached-zero-prompts/
datePublished: 2026-10-05
---
The footer reached the model on 0 of 278 prompts.

The hook is `session_nudges`, wired to UserPromptSubmit. It emits a footer instruction and a few optional sections. The hook timeout was 5s, and live runs took 5.4-8s. Claude Code cancelled the hook on every prompt, so the model never saw a word of it.

## Reading the right record

The `hook_timing` module has a `transcripts` subcommand that reads the harness transcripts and counts success versus cancelled per hook command. It takes about 30s to run. It is the ground truth, and the per-run timing rows the hook writes are the cheap standing measure beside it.

## Where the time went

Live runs took 5.4-8s, and under cProfile the same payload took 27s:

- `nudge_resolver.compose_panel` took 24.6s, and `rank_tasks` inside it took 22.8s.
- `session_attach.find_task_dir` was called 98 times for 15.3s. Each call ran `AREAS.rglob(task_id)` across the entire notes tree, with no cache.
- `footer.find_task_path` was called 70 times for 7.3s, the same pattern, memoized per id only.
- `footer.compose` itself, which does the git and vault subprocess calls, took about 1.4-2.1s. That part was fine.

The expensive piece was an optional ranking panel looking up task directories one id at a time by walking the whole tree.

## The other victims were not the same bug

The transcript table showed other hooks cancelled too. `session_hook prompt` and `units_capture prompt` were profiled standalone.

The `rglob` pattern was not there. `session_hook prompt` ran in 0.61s wall (0.15s profiled). `units_capture prompt` ran in 1.98s wall (0.67s profiled), and most of that was `units.resolve_manifest` globbing at about 0.25s and a 0.14s import of `nudge_resolver`. Their multi-second transcript durations came from machine load and from the hooks being cancelled alongside the slow one. Both hooks were left unchanged. `memory_recall_hook` and `hub_pulse` also show timeouts in the baseline, and we did not investigate them.

## The fix, in two parts

First, `lib/task_index.py`: one `os.walk` per process that builds `{id -> dir}` and `{id -> .md}`, shared by `session_attach.find_task_dir`, `footer.find_task_path`, and `footer.find_task_dir`. It is in-process only, with a 30s TTL. If a hit's path has vanished, it triggers one rescan, and `session_attach` clears it after auto-slotting a folder. The test compares the index against the old `rglob` on a tmp tree, including ids outside the epics, a file where a dir is expected, a missing id, TTL expiry, and the rescan.

After, same payload under cProfile: 4.69s total, `find_task_dir` down to 50 calls and 0.81s (0.77s of that is the single index build), `rank_tasks` at 0.92s.

Second, a budget. Another session had just folded the five UserPromptSubmit hooks into one `prompt_dispatch` process with a 20s timeout. A second settings entry for the footer would have undone that fold, so `session_nudges` now protects its own footer from inside:

- The footer and access mode are built first and never skipped.
- The ranking panel runs in a worker thread with a join budget, `PANEL_BUDGET_S`, 2.5s. Over budget it is dropped and recorded `skipped:over_budget`.
- The four cheap optional sections (checkpoint, handoff, waiting, stepback) are skipped once the run is past `LATE_AFTER_S` (5s) and recorded `skipped:late`.
- Every run appends a row to `.runs/hook-timing/YYYY-MM-DD.jsonl`.

## What the numbers do and do not prove

Wall time of the hook, three runs each, same payload, with the box at about 100% CPU from other sessions' test gates during both measurements:

| | median |
|---|---|
| before | 37.1s |
| after | 4.41s |
| after (second batch) | 4.84s |
| through `prompt_dispatch` (all five hooks) | 4.58s |

Those absolute numbers are inflated by load. Unloaded, the earlier live runs were 5.4-8s, and the after-fix floor is about 3-4s, dominated by the `footer.compose` git calls (about 1.4s) and Python start and imports (0.4s).

The live report shortly after the fix, n=51 rows and mixed with other sessions' load, gave p50 3.8s, p95 8.9s, max 10.8s, with 19 runs over 4s, the panel dropped 2x, and other sections skipped 7x. The machine was never quiet, so that needs re-measuring.

Whether the footer arrives on real prompts is not yet known. Transcripts for prompts after the fix did not exist yet. Success is `session_nudges` cancelled near 0 in the `transcripts` run, and the `over_4s` count small in `report`. If `skipped_sections` shows the panel dropped often, the remaining cost is `recent_sessions` plus `rank_tasks`, each about 0.9s unloaded.

## Takeaway

The hook was cancelled on every prompt, and the profile pointed at uncached tree walks in the ranking panel. The fix added a shared task index and a time budget, and the hook got faster under the profile. Delivery is unproven until the `transcripts` run shows `session_nudges` cancelled near 0 on prompts after the fix.
