Repository navigation
Name guard fails open: lanes-edit.sh guard exceeds its 5 s UserPromptSubmit timeout, and Claude Code cancels it and lets the prompt through #298
Description
Lane: openRepoProject-1
Related: #25 (the guard's adoption, lane protocol Amendment 12; CLOSED) and #52 (its implementation, merged 2026-09-15 as 2faa883, where the code comment quoted in section 4 was written). Amendment 23 act 2 is tracked by #198 (OPEN) and PR #282 (OPEN); see the Sibling search at the end. All figures are from Eagle on 2026-10-20 (UTC) unless a date is given; figures re-read on 2026-10-21 say so.
- "Installed build" = /home/brett/.local/bin/lanes-edit.sh (2026-10-08 20:52, 17,046 lines, 1,007,136 bytes; unchanged on 2026-10-21, sha256 8800a541...).
- "HEAD" = /workspace/projects/openRepoTools at 21e0829 (lanes-edit.sh 18,467 lines, 1,104,141 bytes, +9.6%).
- "origin/main" = a42946e, read with
git ls-remote origin mainon 2026-10-21 (HEAD is its ancestor, 9 commits behind; lanes-edit.sh 18,479 lines, 1,104,831 bytes).
Summary
The guard is registered with "timeout": 5 and no onFailure. When it overruns, Claude Code cancels the hook, discards its output and delivers the prompt (official docs, quoted below). The guard's own fail-closed mapping, every exit other than 0 or 2 becomes 2 (guard) arm, installed lanes-edit.sh:15010-15037, HEAD 16351-16377, origin/main 16363-16389), never runs for a cancelled process. The guard is cancelled up to 640 times a day across the estate (100 or more on 9 of the 11 days from 10-09 to 10-19), and in the one transcript where prompts could be counted, on 23 of 28 turn starts on 10-19 and 10-20. No guard refusal has been recorded since 10-05. The code has said since 2026-09-14 that the timeout is "the one path on which this guard fails open". HEAD and origin/main have the same cost and the same path.
Evidence
1. What Claude Code does (https://code.claude.com/docs/en/hooks; Claude Code 2.1.304 when measured, 2.1.305 on this host since 2026-10-20 20:48Z)
Quoted from the page's raw markdown on 2026-10-20 and re-checked against the live page on 2026-10-21 (every quoted sentence is still there); only the markdown link targets are removed.
Section "UserPromptSubmit":
"
UserPromptSubmithooks have a default timeout of 30 seconds forcommand,http, andmcp_tooltypes, shorter than the 600-second default for those types on most other events. Because this hook runs before every prompt and blocks model processing until it completes, a stuck hook stalls the session.""Apart from a command hook you run with
async: true, aUserPromptSubmitcommand, HTTP, or MCP tool hook that reaches its timeout is canceled and its output, including anyadditionalContext, is discarded. The prompt still reaches Claude without that context. To block the prompt instead, setonFailure: "block"on a command or HTTP hook. The transcript shows a notice naming the hook, the timeout that fired, and that the output was discarded."
Section "Timeouts" (under "Exit code output"):
"Apart from a command hook you run with
async: true, Claude Code cancels acommand,http, ormcp_toolhook that reaches itstimeout, discarding the hook's output, so on most events a timed-out hook renders no decision."
Section "Block the action when a hook fails":
"On most events, when a hook fails or times out, Claude Code still carries out the action. ... To block the action instead, set
"onFailure": "block"on acommandorhttphook. Requires Claude Code v2.1.295 or later.""With
"block"set, a failure does what exit code 2 does on that event ... For example, aPreToolUsefailure blocks the tool call and aUserPromptSubmitfailure blocks the prompt.""After a timeout, the message says
timed outinstead offailed."
Exit codes: only exit 2 blocks a prompt ("Blocks the prompt, so it never reaches Claude"); any other code, "or fails in some other way", is a non-blocking error and "The action goes ahead". So a timed-out guard is cancelled, its output is dropped, and the prompt proceeds. A cancelled process has no exit code for the guard's own mapping to act on.
2. How the guard is registered
{"type":"command","command":"~/projects/xFactory/lanes-edit.sh guard","timeout":5} in /home/brett/.claude/settings.json and in 333 of the 428 settings.json files found up to four levels below ~/.claude-profiles/profiles (recounted 2026-10-21; 331 of the 333 are the team profiles at depth four; the 2026-10-20 count was 331 of 393, over a file set that was not recorded). None mentions onFailure (95 files carry no guard entry). ~/projects/xFactory/lanes-edit.sh is a symlink to /home/brett/.local/bin/lanes-edit.sh (sha256 8800a541...). The installer writes the entry from openRepoTools:2105-2116 at HEAD, 2343-2354 at origin/main (GUARD_COMMAND, GUARD_TIMEOUT=5, guard_block_text; re-read 2026-10-21) and has merged it since 2faa883 (2026-09-14).
3. What the transcripts show
Hook attachments of type hook_cancelled, timedOut: true, timeoutMs: 5000, command "~/projects/xFactory/lanes-edit.sh guard", counted per day over every transcript under ~/.claude-profiles/state/opensoft/projects (3,707 files changed since 10-01):
before the 2026-10-08 20:52 install: 10-05 2 | 10-06 1 | 10-07 5 | 10-08 0
after: 10-09 442 | 10-10 18 | 10-11 459 | 10-12 330 | 10-13 114 | 10-14 7 | 10-15 112 | 10-16 264 | 10-17 471
10-18 640 | 10-19 186 | 10-20 25 (to 09:30Z)
- Median cancelled duration about 5.5 s (the 5 s kill plus about 0.5 s); maximum 24.7 s (10-18).
- No
hook_blocking_errorfor the guard in any transcript since 10-05. session-start, the offline SessionStart hook (alsotimeout 5), is cancelled too (51 on 10-18, 35 on 10-19). When it succeeds it takes 3-5 s; on 09-13 it took 0.07-0.65 s.- One instance, transcript /home/brett/.claude-profiles/state/opensoft/projects/-workspace-projects-openRepoProject/a0e10447-aab0-411d-8015-359674fb42ea.jsonl, lines 2795-2800: a prompt is queued at 09:27:37.131Z, the guard is
hook_cancelled/timedOutat 09:27:42.662Z (timeoutMs 5000, durationMs 5519), the assistant acts on the prompt at 09:27:47.715Z. - In that transcript the denominator can be counted (turn starts = user text records plus queued commands, including subagent reports, which also fire UserPromptSubmit per the docs). A turn start counts as cancelled when a guard cancellation follows 3.5-10 s later: 23 of 28 on 10-19 and 10-20 (82%), 2 of 9 on 10-18, 0 of 16 on 10-11 with the same installed build (UNCONFIRMED whether the guard was registered for that profile on 10-11, or finished in time). A silent pass leaves no record, so the share of prompts the guard finishes cannot be counted from transcripts.
- In the same session
usage-guard.sh, the other UserPromptSubmit hook, finished in 121 ms in a recorded run.
4. What the code says about itself
lanes-edit.sh:8407-8414 (HEAD and origin/main 9114-9121), the same text in all three builds; it arrived with 2faa883 (#52, 2026-09-14):
WHAT IT COSTS, AND THE ONE WAY IT FAILS OPEN. Measured on Eagle 2026-09-14 against 329 session records and a 1.3 MB register: the register read is ~2 s wall and the
grep -lover every profile's records ~0.4 s, so a refusal lands inside the ratifiedtimeout 5. A hook that EXCEEDS its timeout is killed, and a killed hook does not exit 2 -- so the timeout is the one path on which this guard fails open. It is the amendment's own number and is left at it; ...
(The dash is ASCII-fied.) Today there are 466 session records (+42%) and the register blob on origin/main is 2,358,766 bytes (+81%).
5. Measured on 2026-10-20, 11:19-11:22Z (one sample each; the host was busy)
- The exact call
session_filesmakes on the big tree (find "$HOME/.claude-profiles/profiles" -path '*/sessions/*.json' -type f; lanes-edit.sh:3873-3896, HEAD 4247-4270) found 466 records in 28,819 directories. First pass 4.681 s. Repeats 0.913, 0.831 and 0.663 s. One cold pass alone is 94% of the 5 s budget. - Host at that time: load average 78 / 98 / 76; memory pressure, PSI memory
someavg300 31.7% andfullavg300 30.1%; free 0.8 GB, page cache 45 GB, 4 GB of 14 GB swap in use; /dev/sdd 94% full (891G of 1007G). - Process spawn was cheap then: about 2 ms per process and about 6 ms per
jstrcall (four processes). So spawn count is not what costs time at present. - A three-level glob (
profiles/*/*/*/sessions/*.json) took 0.654 s against 0.663 s for the find, and missed 1 of 466 records (profiles/xfactory-001/sessions/3506923.json, one level up). bash -n(parse only) median of 9 runs: 42.9 ms installed, 41.8 ms HEAD.
6. Is it fixed at HEAD or at origin/main? No.
guard_run at HEAD (10069-10538) differs from the installed one (9290-9736) by 7 added and 4 removed code lines. Cost: -1 jnum (installed line 9385 assigns gr_since and never reads it; -4 processes); +1 full awk pass over the register (a new test that the register text is readable, register_text | awk 'NF{t=1} END{exit !t}'; the first register render moves to that point, so the render itself is not extra); record_window_state adds one tmux display-message only when a window is not found; log_files_named_ci adds one subshell; transcript_holders adds shell bookkeeping only. The register read's cold path makes the same three git calls (ls-tree replaces cat-file -e). Identical in both: session_files (the whole-tree find), jstr, jnum, pid_alive, pid_under, here_context, lane_binding_scan, binding_window_state, lane_alias_text, rows_named_ci_alias, the guard) arm (HEAD 16351-16377, origin/main 16363-16389, no deadline), and all load-time code before dispatch.
Failure mode, all toward fail-closed on a failed read: HEAD refuses (exit 2, correct reason) when the register text cannot be read, where the installed build fell through to the last table arm with a cure that cannot mend a read that did not happen; rows_named_ci and log_files_named_ci pass a failed read's status on; record_window_state answers 2 ("no answer") instead of 1 ("gone") when no tmux server answers. Choice 2 (guard_answer, installed 9943-10087, HEAD 10754-10906) now runs lane-start --no-launch --bind "$G_ID" and no longer runs a second writer (guard_bind_uuid -> append-session-id), but that one writer still runs synchronously inside the hook.
So HEAD would still overrun for the same reasons (by code comparison; unmeasured, because the guard was not run during this investigation). An install of HEAD would not fix this issue.
origin/main (a42946e, re-read 2026-10-21) has the same guard code. Its lanes-edit.sh differs from HEAD's in one place only, inside lane_reconcile (starts at 13851; HEAD lines 14196-14214; 12 more lines; called only from the reconcile verb at 18394, not from the guard). So the guard) arm moves from 16351-16377 to 16363-16389 and no other line cited here moves. session_files (4247-4270), the comment at 9114-9121, transcript_holders (9342-9460), lane_binding_scan (9506-9633), guard_run (10069-10538) and guard_answer (10754-10906) are at the same lines at HEAD and origin/main.
Root cause
- The budget is fixed and the cost is not. The entry says
timeout 5(Claude Code's own default for this event is 30 s). The agreeing path loads and parses a 1 MB script, then runstranscript_holders(installed 8585-8683): the whole-tree find above,xargs grep -l -Ffor the session id across 466 records, and for each matching record about 20 processes (jstr,kill -0and/proc/<pid>/stat,tmux display-message, an ancestry walk of up to 64/procreads); thenrows_named_ci_aliastwice (window name, session name) plussession_ids_of_laneandrow_of_lane, each an awk pass over the 2.36 MB register;lane_binding_scan(8729):git ls-tree,git showof the lane's 64 KB log, awk, tmux.guard_runhas 6 lines that calljstrand 1 that callsjnum(4 processes each). - Most of that is cache-sensitive, and this host has sustained memory pressure. The one step timed end to end costs 0.66-0.91 s warm and 4.68 s on a cold pass.
- The code accepted the fail-open path in 2026-09-14 because the measured cost was about 2.4 s (section 4). The data has grown by 42% (records) and 81% (register) since, and the cost now often passes 5 s.
- On the overrun Claude Code cancels the hook, discards its output and delivers the prompt (section 1). The guard's fail-closed mapping (the
guard)arm turns every code other than 0 or 2 into 2, Amendment 12(d)) cannot act on a process that was cancelled. Nothing else in the entry (noonFailure) makes a timeout block. - The guard does not fetch and takes no lock, except on choice 2 (
guard_answer), which runslane-start --no-launch, a writer with fetch and push (LANES_GIT_TIMEOUTdefault 60 s, up to 6 attempts), synchronously inside the hook.
Not established: which in-guard step crosses 5 s on a given prompt. No end-to-end profile exists: the guard was not run outside a live prompt during this investigation. What changed on 10-09 is also unattributed: no lanes-edit.sh commit between 10-04 and 10-09 names the guard (7 commits: worktrees, derived index, ctx, crash-consistent recovery), and the installer has merged the guard entry since 2026-09-14. The step from 0-5 to 442 cancellations a day may be environment (cache, load, disk), a per-profile rollout, or growth; it is UNCONFIRMED which.
Impact
- The Amendment 12 guarantee (fail closed, clause (d)) is not what runs on a prompt the guard does not finish. A window, session name and register row that disagree can pass. UNCONFIRMED whether any real mismatch passed in this period; no refusal has been recorded since 10-05.
- A cancelled guard still costs the person about 5.5 s on that prompt, and the hook "blocks model processing until it completes" (docs). session-start adds up to 5.5 s at launch and resume.
- Choice 2 can be cancelled in the middle of a
lane-start --no-launchwrite. What the harness does to that child process is not in the docs and is UNCONFIRMED. - Nothing tells the person: the only trace is a
hook_cancellednotice in the transcript.
Proposed fixes, ranked
Rank is the order to land them. Fixes 2 and 3 turn a slow guard into a prompt-blocking guard, so with today's cost they would refuse most prompts. Fix 1 must land first or with them.
- Make the agreeing path cheap and independent of cache state. Target: p95 under 1 s warm and under 2.5 s cold, on the data above, at load average 10 or more.
- Do not walk 28,819 directories at every prompt (
session_files). The guard needs this window's own record and the Amendment 18(h) other-holder check. Read its own record directly (the harness pid is an ancestor of the hook, and the record issessions/<pid>.jsonunder$CLAUDE_CONFIG_DIR), and answer the cross-profile check from a small index thatlanes-indexor session-start keeps, or from a glob of the roughly 100 knownsessions/directories. The layout is not uniform, so bothprofiles/*/*/*/sessionsandprofiles/*/sessionsare needed. Warm, a glob took the same time as the find, so the saving is expected only cold (about 100sessions/directories and the few above them, instead of 28,819 directories) and is unmeasured. - Read the register once into a file and answer each lookup with one awk. Today it is at least five passes over 2.36 MB (six at HEAD). Skip the alias read when the window's name has a row.
- Replace
jstr's four-process pipeline with the one-awk parse that already exists (SESSION_RECORD_AWK, installed line 3927, HEAD 4301). - Make the guard write one line per run (elapsed ms, exit code) to a local file. Silent passes leave no record today, so no one can measure the guard's real distribution.
- Do not walk 28,819 directories at every prompt (
- An internal deadline below the hook budget that exits 2 itself. Start a watchdog at the top of the
guard)arm (for exampleLANES_GUARD_DEADLINE, default 4 s). When it fires it kills the guard's process group, printsthe name guard could not finish in <n> s; this prompt is refused (Amendment 12(d))with the safe-mode line, and exits 2. A deliberate exit 2 is the only way the person sees a reason: a cancelled hook's output is discarded. "onFailure": "block"on the guard's UserPromptSubmit entry, as the backstop when the guard dies for any reason (cancel, cannot start, exit 1 or 127). Claude Code v2.1.295 or later (2.1.305 here; other hosts UNCONFIRMED). It touches: the installer (guard_block_text, openRepoTools:2346-2354 at origin/main (2108-2116 at HEAD), andplan_hook_merge, 3150-3407 at origin/main (2597-2854 at HEAD), where an existing entry with the exact command string counts aspresentand is not updated, so a migration of the existing entries is needed), the workBenches launcher that ensures the same two strings in each profile's settings.json (331 files today), the five test files that mention the entry (tests/test_install_skill_and_hook.py, test_install_hook_gate.py, test_install_payload_source.py, test_skill_shell_blocks.py, test_repo_hygiene.py; not read for what they assert; the same five mention it at origin/main), and Amendment 12 adoption act 3's text. It needs Brett Heap's word. Not before fix 1.- Never run a writer inside the guard. Choice 2 should print the
lane-start --no-launchcommand (the refusal tables already print it as the cure), or start it detached and report next prompt, instead of running a fetch and push loop under a 5 s budget where a cancel can land mid-write. - A larger hook timeout (for example 15 s). This is a protocol change (Amendment 12 adoption act 3, "timeout 5"), needs Brett Heap's word, hides the cost, and makes every prompt wait. Stopgap only. It may not even suffice: cancelled hooks were seen taking up to 24.7 s to stop, so a natural completion time above 15 s under load is possible (UNCONFIRMED).
Acceptance criteria
- On the data above (466 records, 2.36 MB register), with caches cold (after a reboot or a dropped cache on a test host) and with load average 10 or more, the agreeing path finishes in under 2.5 s, and in under 1 s warm, at p95, read from the guard's own timing line.
- A test seam that slows one read (a sleep) makes the guard exit 2 with the deadline message before the hook's timeout. A suite case asserts exit 2 and the message.
- Over a 24-hour working day after the install, no transcript carries a
hook_cancelledwithtimedOut: truefor the guard or for session-start. - Choice 2 makes no network call and starts no writer inside the hook process. A suite case asserts it, for example with the fake git logging no fetch and no push.
- No
findover$HOME/.claude-profiles/profileson the guard's call path (a suite case reads the call graph). - If
onFailureis adopted: the entry in /home/brett/.claude/settings.json and in every profile settings.json carries it, and a guard killed after N s shows the prompt blocked on Claude Code 2.1.295 or later (a manual check). - session-start finishes in under 1 s on the same data.
Not confirmed
- Which in-guard step crosses 5 s on a given prompt (no end-to-end profile). Timed pieces: the cold find (4.681 s, once), the warm find (0.66-0.91 s),
git showof the register (15 ms, measured earlier at load about 14), parse (43 ms), spawn (2 ms). - The share of prompts on which the guard finishes in time, across the estate (82% cancelled on 10-19 and 10-20 in one transcript).
- What changed on 10-09 (see Root cause).
- The cold-cache benefit of a glob or a direct record read (expected, unmeasured).
- What the harness does to child processes of a cancelled hook, and whether
onFailurebehaves as documented on this deployment (docs only; not tested). - That HEAD overruns (by code comparison only).
- Not refreshed on 2026-10-21: the per-day cancellation counts (to 2026-10-20 09:30Z), the 23-of-28 turn count, the timings and the host state in section 5.
- The subject and state of skills/handoff/SKILL.md:310-315: the handoff commit is a bare
git commitin a spelled ~/projects/brett-wip, so it can publish other lanes' staged work and stop mid-rebase: resolve the checkout, commit by pathspec, refuse foreign staged entries #197 (named by Amendment 23 act 2; none of the sibling-search queries returned it), and the content of Lane writers commit first and sync second, so a pull that refuses or conflicts strands the commit and later writes stack on it: fast-forward before the edit, and refuse before committing (fail closed) #198, PR Every register write goes through its lane's own checkout <W>-lanes/<lane>/, where lane-start and lane-end read the lane's row; a LANES_REPO naming W refuses; locks name their holder's container; refresh-workspace fast-forwards W (Amendment 23) (#198) #282 and every other hit of the sibling search, which were read by title and state only.
The installed tools are out of date, and Amendment 23 S3 and S4 are in no build
What is installed, against HEAD and origin/main
| file(s) in ~/.local/bin | installed | against HEAD 21e0829 and origin/main a42946e |
|---|---|---|
| lanes-edit.sh, lane-start, lane-end, lanes, lanes-index, lane-worktrees, openRepoTools | 2026-10-08 20:52 | differ |
| lane, lane-handoff, claude-current, claude-restart-check | 2026-10-06 14:19 | differ |
| park, resume, status | 2026-09-15 13:52 | differ |
| lane-rename, link-estates, repos.tsv | 2026-10-05 / 2026-09-15 | identical |
HEAD's lane-start, lane-handoff and lane-worktrees are 2026-10-19; its lanes-edit.sh is 2026-10-19 22:01. The checkout showed no change on 2026-10-21. origin/main is a42946e (git ls-remote origin main, 2026-10-21 about 10:48Z; the local ref agrees), 9 commits ahead of HEAD. Against HEAD it changes openRepoTools (619 lines: pipx package mode, "--install leaves pipx's link to pipx"), lane-handoff (62 lines), lane-worktrees (95) and lanes-edit.sh (18 lines, 12 added in lane_reconcile at 14197; the guard path is unchanged), and adds pyproject.toml, src/openrepotools/ and the release workflows. lane-start, lane-end, lane, lanes, lanes-index, claude-current, claude-restart-check, park, resume, status, lane-rename, link-estates, repos.tsv, skills/ and commands/ are byte-identical to HEAD's.
Amendment 23 S3 and S4
Rules: /home/brett/.agents/protocols/lane-collision-protocol-amendment-23.md (fetch, refuse on unpublished commits, merge --ff-only, reset --keep replay, never pull --rebase). Occurrences by grep -o -F, shown as installed / HEAD / origin/main a42946e (all three counted the same way on the file or the blob; re-run on 2026-10-21, same figures as before):
| tool | merge --ff-only |
reset --keep |
git cherry |
pull --rebase |
|---|---|---|---|---|
| lanes-edit.sh | 0 / 0 / 0 | 0 / 0 / 0 | 3 / 3 / 3 | 21 / 27 / 27 |
| lane-start | 0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 | 4 / 4 / 4 |
| lane-handoff | 0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 | 2 / 2 / 2 |
| lane-worktrees | 0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 |
(lane-end and lane: all four strings 0 in installed and HEAD.) No tool in ~/.local/bin, in the checkout or at origin/main implements S3 or S4, and none names S1's per-lane checkout path (0 hits for brett-wip-lanes, $W-lanes, .partial, at origin/main too). PR #282 (OPEN, not merged) is the work that names it.
- The executed
pull --rebasecalls are: lanes-edit.shcommit_push(installed 2092, HEAD 2297, withrebase --abortat 2116 / 2321,sleep 3at 2160 / 2370 and the six-attemptdie ... 6at 2162 / 2372); lane-start (installed 4144, HEAD 4422); lane-handoff (installed 2132, HEAD 2636, origin/main 2676; the lanes-edit.sh and lane-start lines are the same at origin/main). The other mentions are comments and printed hints. - The three
git cherryhits are two comment lines (installed 2141, 2146; HEAD 2346, 2351) and one printed recovery hint (2151 / 2356). None is the S3 "unpublished commits" refusal test. - The only reset either build suggests is a printed
git reset --hard origin/<branch>(installed 2154, HEAD 2364), where S3 prescribesreset --keep. - lane-worktrees is not a register writer, so its zeros are not a gap.
The amendment's act 2 (the tooling: S1 resolver, S2 in every writer, S3-S4, S5, S6) has not landed in any build examined here (installed, HEAD, origin/main).
What openRepoTools --install would change (read from the code and cmp -s; NOT run)
From the checkout (HEAD 21e0829; /workspace/projects/openRepoTools/openRepoTools; payload_select finds all 23 payload files beside it, so local mode: the checkout's bytes, no network):
- ~/.local/bin, 17 files (INSTALLABLES, line 1077; 1292 at origin/main): UPDATED, 14: openRepoTools, park, resume, status, lane, lanes, lane-handoff, lanes-edit.sh, lane-start, lane-end, claude-current, claude-restart-check, lanes-index, lane-worktrees. Unchanged (mode stamp only), 3: lane-rename, link-estates, repos.tsv. This replaces every 2026-10-06 and every 2026-10-08 build there.
- Skills and commands, 12 destinations: ~/.claude-profiles/shared/skills/{handoff,lane-swap,restart}/SKILL.md and ~/.claude/skills/{...}/SKILL.md; ~/.claude-profiles/shared/commands/{handoff,ctx,swap}.md and ~/.claude/commands/{...}.md. Only
handoffSKILL.md differs, at both paths (2 files updated); the other 10 are unchanged. - ~/.claude/settings.json: both entries are already present by exact command string and the mode is already 600, so nothing is written. The installer's "answers
guard" probe is an awk text search of the script, so the guard is not run. It never writes a profile's settings.json (the launcher owns those). - Receipt /home/brett/.local/share/openRepoTools/installed.tsv (29 rows, 2026-10-08T20:52:08Z) is rewritten.
restartis not in ~/.local/bin, so nothing is retired. jq 1.7 and sha256sum are present, so no prerequisite refusal. - Mechanism: each file is written beside its target and renamed over it (
place_by_rename, line 1810), so a tool already running finishes on its old inode. - Reach: the hook path ~/projects/xFactory/lanes-edit.sh is a symlink into ~/.local/bin/lanes-edit.sh, so after the rename the next hook run in every live session on this workstation uses HEAD's lanes-edit.sh at once, and every later lane-start, lane-handoff and lane-end is HEAD's.
- Other invocation forms, from
payload_select(not run): the installed ~/.local/bin/openRepoTools--installfinds no skills beside itself, so it re-copies the 17 files beside it (no change to the stale tools) and fetches the skills and command files from GitHubmain; the README'scurl ... | bash -s -- --installfetches everything frommain, which is a42946e today, 9 commits past HEAD (its installer has the pipx mode; README line 228 still gives the command). - Re-read on 2026-10-21 against origin/main a42946e. The same
cmp -sof the origin/main files against ~/.local/bin gives the same result: 14 would be updated, 3 unchanged (lane-rename, link-estates, repos.tsv); thehandoffSKILL.md differs at its two paths and the other 10 skill and command files are the same. It is the same because the four files that differ between HEAD and origin/main (openRepoTools, lane-handoff, lane-worktrees, lanes-edit.sh) already differ from the installed ones. New at origin/main is pipx mode (Releases: annotated tags, release/vX.Y branches, and openrepotools on TestPyPI and PyPI from v1.1.0 #228): if ~/.local/bin/openRepoTools is pipx's link,--installleaves it to pipx and counts the other sixteen. Here it is a regular file (stat), so all 17 would be placed as above. The installer's line numbers at origin/main:payload_select1192,place_by_rename2025,install_commands2065-2200, the guard entry 2343-2354,plan_hook_merge3150-3407 (HEAD: 977, 1810, 1850-1962, 2105-2116, 2597-2854). Thepresenttest inplan_hook_mergeis still by command string alone, so a re-install does not update an existing guard entry.
Consequence
Installing HEAD or origin/main would neither fix the guard timeout (section 6) nor deliver Amendment 23 (S3 and S4 are in no build). It would replace the guard workstation-wide with a build that is 9.6% larger (HEAD; 9.7% at origin/main) and has the same cost and no deadline. Track Amendment 23 act 2 through #198 (OPEN), PR #282 (OPEN) and #197 (named by the amendment; state not established), and land the guard fixes above before or with any install.
Sibling search
Searched on 2026-10-21 (about 10:49Z) in opensoft/openRepoTools, issues and pull requests in every state, with GitHub text search (title, body, comments): one request that ran 14 separate queries. Hits per query: UserPromptSubmit 17; onFailure 0; "fail open" 36; "fails open" 5; "fail-open" 36; "name guard" 60 (40 returned); guard timeout 33; guard "timed out" 12; hook_cancelled 0; "hook timeout" 1; "timeout 5" 4; guard overrun 0; guard "fail closed" 46 (40 returned); lanes-edit.sh guard 88 (40 returned). A second request, to read the bodies and comments of the 60 most likely hits, failed with HTTP 502 and was not repeated, so no hit's body was read: the judgments below rest on title, state, last-update date and the words the search matched. On 2026-10-20 a search limited to issues (gh issue list --state all --search "guard timeout UserPromptSubmit") had returned one issue, #25.
- No existing issue or PR reports this. The terms that single it out,
onFailure,hook_cancelledandguard overrun, matched nothing. Nor do they appear in the tracked files or the commit messages at origin/main (git grep -i -E 'onFailure|hook_cancelled'at a42946e andgit log -i -E --grepfor the same: no match). The only tracked statements that the guard times out or fails open are the code comment atlanes-edit.sh:9114-9121(and a mention of "the amendment'stimeout 5" at 10370), anddocs/README-lanes.md:2409, which gives the entry as "timeout 5". - Name guard (lane protocol Amendment 12): lanes-edit.sh guard as a UserPromptSubmit hook, lane-start --name on every launch, --install places the hook — after #24 lands #25 (CLOSED) and The name guard refuses a prompt whose window, session name and register row are not ONE lane — the lock types
/rename <lane>into the pane, a person's rename is an OFFER answered at the next prompt, and a transcript two live processes hold is a refusal in the guard, inlane-startand inlane-end --retire#52 (MERGED as 2faa883): the origin, not duplicates. They adopted the guard and the 5 s entry, and The name guard refuses a prompt whose window, session name and register row are not ONE lane — the lock types/rename <lane>into the pane, a person's rename is an OFFER answered at the next prompt, and a transcript two live processes hold is a refusal in the guard, inlane-startand inlane-end --retire#52 is the commit that wrote the comment calling the timeout "the one path on which this guard fails open" (git log -S'a killed hook does not exit 2'returns 2faa883 alone). Both were last updated on 2026-09-15, three weeks before the first recorded cancellation (2026-10-05) and before the rise from 2026-10-09, so neither can report it. The comment records the path as an accepted limit, measured then at about 2.4 s; this issue is about what the cost is now. - Other name-guard issues and PRs, on other subjects: Amendment 18 tooling: one binding per lane — host/os/container sub-fields, lanes-edit.sh binding + request-handoff, HANDOFF-REQUESTED, the guard's two reads, lane-start/restart ask → wait → refuse → --force — after #25 and #36 #38 (CLOSED, Amendment 18: the guard's two reads), The name guard's two remaining indeterminate reads pick a branch of the (b) table instead of naming the read: the register's TEXT and the lane's binding log #60 (CLOSED) and The name guard and the SessionStart hook name an unreadable register or binding log instead of guessing a branch (#60) #208 (MERGED) (an unreadable register or binding log is named, not guessed: that is the fail-closed handling of a failed read inside a run that finishes, which a cancelled run never reaches), The name guard offers a readable, recoverable lane/session mismatch three choices, and lane-start resumes an exact transcript uuid, never a title #87 (the three-choice offer), Skip the lane name guard for profile-only launches #135 (skip the guard for profile-only launches), A silent tmux is no longer read as a dead window, the companion verdict is read off the lane's live window from any window, and the guard's cure for a lane named before Rule 4 says what it cannot fill in (#73) #222 (a silent tmux is no longer read as a dead window; the guard's cure wording) and lane-start gains a guard-only binding mode that stamps the transcript being bound, so the offer's yes no longer records a RESUMED nobody is in (#70) #223 (lane-start's guard-only
--bindmode, which choice 2 runs and fix 4 here would take out of the hook), Fresh install activates Claude name-guard hook before workspace linking supplies its target #237 (CLOSED) and --install and wip init add the hook pair only once ~/projects/xFactory/lanes-edit.sh answers guard, and take our guard entry back out until then, so a fresh install never leaves a prompt hook calling a missing command (#237) #247 (MERGED) (the installer adds the entry only once the hook path answersguard: the same entry that fix 3 here would change). Testing: two suites at once break the shell suite, and the guard every brief used never waited — document the serialization and ship a wrapper that takes a lock #51 (CLOSED) uses "guard" for the test-suite lock, not this guard. lane's pick: the pty driver gets its own 240 s bound and reports a timeout as one; the lanes read's quadratic lookup is made linear (57 s → 3 s) #146 (MERGED) madelanes's read linear (57 s to 3 s), a precedent for a measured speed fix, on another command. None concerns the timeout. - Open items on the same surfaces, not duplicates: Amendment 22 act 2: the SQL register under lanes-edit.sh — mode=primary writers in one transaction each, the estate switch and lanes/STORE, the write journal and the renderer (one mirror commit per write), exit 69, fail-closed readers, lanes-index import/rollback, SERIALIZABLE claim order, and the agreement tests #165 (OPEN, Amendment 22 act 2, the SQL register under lanes-edit.sh with "fail-closed readers"; it matched
UserPromptSubmit; if it lands it changes how the register is read, which is fix 1's register work); Lane writers commit first and sync second, so a pull that refuses or conflicts strands the commit and later writes stack on it: fast-forward before the edit, and refuse before committing (fail closed) #198 (OPEN, last updated 2026-10-20) and PR Every register write goes through its lane's own checkout <W>-lanes/<lane>/, where lane-start and lane-end read the lane's row; a LANES_REPO naming W refuses; locks name their holder's container; refresh-workspace fast-forwards W (Amendment 23) (#198) #282 (OPEN, last updated 2026-10-21 10:34Z, "(Amendment 23) (Lane writers commit first and sync second, so a pull that refuses or conflicts strands the commit and later writes stack on it: fast-forward before the edit, and refuse before committing (fail closed) #198)"), which, by the title, move every register write and the register reads of lane-start and lane-end to the lane's own checkout,<W>-lanes/<lane>/(Amendment 23), and may move the line numbers cited here if it lands; Automatic 65% context rollover through safe semantic /ctx #92 (OPEN, automatic 65% context rollover; matchedUserPromptSubmit); lane-end close-out: what #175 leaves of #163 — landing retires its own tree, the guard's two checks and ignored-size threshold,--sweep --yesafter #170, lane-start recording a lane's existing trees #181 (OPEN; by its title the "guard" there is lane-end's close-out gate). skills/handoff/SKILL.md:310-315: the handoff commit is a baregit commitin a spelled ~/projects/brett-wip, so it can publish other lanes' staged work and stop mid-rebase: resolve the checkout, commit by pathspec, refuse foreign staged entries #197, which Amendment 23 act 2 names, was returned by none of the queries; its subject and state are not established. - Result: no duplicate was found, so this is filed as new. The nearest issues are Name guard (lane protocol Amendment 12): lanes-edit.sh guard as a UserPromptSubmit hook, lane-start --name on every launch, --install places the hook — after #24 lands #25 and The name guard refuses a prompt whose window, session name and register row are not ONE lane — the lock types
/rename <lane>into the pane, a person's rename is an OFFER answered at the next prompt, and a transcript two live processes hold is a refusal in the guard, inlane-startand inlane-end --retire#52, which are closed and are where the timeout and the comment came from. What the search cannot exclude is a body or comment that reports the overrun in words none of the 14 queries matched; the three terms that name the failure (onFailure,hook_cancelled,guard overrun) returned nothing in any issue or PR.
Activity