Windows: five install and hook defects, with a tested patch #4

Open
opened 2026-09-13 14:45:35 +02:00 by solvreven · 0 comments

Field report from installing f498b6f on 2026-09-12 into a real, busy repository (Python app, ~1,500 tests, background jobs that write tracked data files, a 120 KB CLAUDE.md).

Environment: Windows 11, Git for Windows (bash 5.3.9, core.autocrlf=true from
the system gitconfig), Python 3.14 (python3 is the WindowsApps alias and works),
Claude Code 2.1.269 → 2.1.270.

How it was checked: the kit's own suite; the hooks driven exactly as
settings.json holds them; and four live headless claude -p sessions reading
the hook_response events. Nothing here was run on Linux or macOS.

Fixed in the patch below

Kit suite: 90 passed / 3 failed → 101 passed / 0 failed on Windows. Every new
assertion was proven RED against unpatched f498b6f code before being trusted.

A1. Three test-install.sh assertions can never pass on Windows

test-install.sh:24, :46, :88 embed the settings path inside the Python source:

python3 -c "import json;json.load(open('$s'))"

MSYS converts paths in argv for native executables, but not paths inside a
string, so native Python receives /tmp/tmp.XXXX/... and raises
FileNotFoundError. The installer itself is fine: it passes the path as argv.

The consequence is worse than a red suite. These three stay red identically
when the settings merge is genuinely broken (duplicated hooks, invalid JSON),
so on Windows they cannot detect the regressions they exist for.

Fix: pass the path as sys.argv[1].

Related: the "hooks not duplicated" assertion counts entries, not
commands. A merge that nests the same command three times inside one entry
stays green (mutation-tested). The patch adds a per-command count.

A2. The Stop prompt sends agents to log.md, which the contract calls read-only

wiki-stop-guard.sh says (update page, crosslinks, index.md, log.md), while
WIKI.md has said since e8f0e76 that "A log.md from older installs is
read-only history"
. The log-per-entry migration missed this line.

Fix: the prompt now reads a new entry in <wiki>/log/, with tests that it
names wiki/log/ and not log.md.

A3. The SessionStart page count uses the project path as a glob

find "$dir" -name '*.md' -not -path "$dir/raw/*" -not -path "$dir/log/*"

-path takes a pattern, so a [ or a backslash in the absolute project path
makes both exclusions match nothing. Log entries and raw sources are then
counted as pages, with no error.

Reproduced two ways:

  • CLAUDE_PROJECT_DIR given as a backslash path (cygpath -w): the hook reports 4 pages instead of 3.
  • A forward-slash path containing proj[1]: same result.

It is latent on Claude Code 2.1.269, which exports forward slashes. Any user
whose path contains brackets gets a count that grows with every log entry.

Fix: cd "$dir", then use relative patterns. A new test installs into a
directory named proj[1].

A4. The Stop guard misreads quoted porcelain paths

The guard classifies inside/outside with substr($0, 4, ...) over
git status --porcelain. Git quotes paths containing spaces or non-ASCII
bytes (?? "wiki/notes/gy\303\266keres.md"). The column-4 match then fails, a
new wiki page counts as outside the wiki, and the agent is told it recorded
nothing right after recording something.

Fix: two pathspec-limited status calls:

  • outside: -- ':(top)' ":(exclude)$wiki"
  • inside: -- "$wiki"

The existing subdirectory-rooted tests still pass. A new test uses a non-ASCII
page name, with a precondition asserting that git really quotes it.

A5. The installer writes settings.json with CRLF on Windows

open(path, "w") is text mode. Fix: newline="\n". The test checks bytes,
because Git for Windows' grep does not reliably see a trailing CR: the first
grep $'\r' version of this assertion could not fail.

The patch (applies to f498b6f, 134 lines)

diff --git a/install.sh b/install.sh
index 7d8cd7d..801b1e0 100755
--- a/install.sh
+++ b/install.sh
@@ -93,7 +93,7 @@ for event, cmd in wanted.items():
     )
     if not present:
         entries.append({"hooks": [{"type": "command", "command": cmd}]})
-with open(path, "w") as f:
+with open(path, "w", newline="\n") as f:
     json.dump(settings, f, indent=2)
     f.write("\n")
 PYEOF
diff --git a/template/claude/hooks/wiki-session-start.sh b/template/claude/hooks/wiki-session-start.sh
index 0d2d7b1..498ed1b 100644
--- a/template/claude/hooks/wiki-session-start.sh
+++ b/template/claude/hooks/wiki-session-start.sh
@@ -3,7 +3,9 @@
 # stdout is injected into the session's context.
 dir="${CLAUDE_PROJECT_DIR:-.}/{{WIKI_DIR}}"
 [ -d "$dir" ] || exit 0
-count=$(find "$dir" -name '*.md' -not -path "$dir/raw/*" -not -path "$dir/log/*" 2>/dev/null | wc -l | tr -d ' ')
+# Patterns relative to the wiki dir: -path reads its argument as a glob, so an
+# absolute $dir containing '[' or a backslash silently disables the exclusions.
+count=$(cd "$dir" 2>/dev/null && find . -name '*.md' -not -path './raw/*' -not -path './log/*' | wc -l | tr -d ' ')
 last=$(ls "$dir/log/" 2>/dev/null | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}-' | sort | tail -n 1 | cut -c1-10)
 [ -n "$last" ] || last=$(grep -E '^- [0-9]{4}-' "$dir/log.md" 2>/dev/null | tail -n 1 | awk '{print $2}')
 cat <<EOF
diff --git a/template/claude/hooks/wiki-stop-guard.sh b/template/claude/hooks/wiki-stop-guard.sh
index f5ca171..6b4bc73 100644
--- a/template/claude/hooks/wiki-stop-guard.sh
+++ b/template/claude/hooks/wiki-stop-guard.sh
@@ -33,15 +33,15 @@ marker="${TMPDIR:-/tmp}/wiki-stop-guard-${session_id}"
 [ -e "$marker" ] && exit 0
 [ -d "$proj/$wiki" ] || exit 0
 git -C "$proj" rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0
-prefix=$(git -C "$proj" rev-parse --show-prefix 2>/dev/null) || exit 0
-
-status=$(git -C "$proj" status --porcelain 2>/dev/null) || exit 0
-# porcelain: 2 status chars + space, path starts at column 4
-outside=$(printf '%s\n' "$status" | awk -v d="${prefix}${wiki}/" 'NF && substr($0,4,length(d)) != d' | head -n 1)
+# Pathspecs, not column parsing: porcelain quotes paths containing spaces or
+# non-ASCII bytes, and a substr() on column 4 reads those as outside the wiki.
+# ':(top)' keeps "anything in the repo" for subdirectory-rooted projects; the
+# wiki pathspecs resolve relative to $proj.
+outside=$(git -C "$proj" status --porcelain -- ':(top)' ":(exclude)$wiki" 2>/dev/null) || exit 0
 [ -n "$outside" ] || exit 0
-inside=$(printf '%s\n' "$status" | awk -v d="${prefix}${wiki}/" 'substr($0,4,length(d)) == d' | head -n 1)
+inside=$(git -C "$proj" status --porcelain -- "$wiki" 2>/dev/null) || exit 0
 [ -n "$inside" ] && exit 0
 
 touch "$marker" 2>/dev/null
-printf '{"decision": "block", "reason": "Before finishing: this session changed files but recorded nothing in the wiki. If you learned something durable the next agent could not cheaply re-derive, record it now per %s/WIKI.md (update page, crosslinks, index.md, log.md). If nothing durable was learned, finish normally."}\n' "$wiki"
+printf '{"decision": "block", "reason": "Before finishing: this session changed files but recorded nothing in the wiki. If you learned something durable the next agent could not cheaply re-derive, record it now per %s/WIKI.md (update page, crosslinks, index.md, a new entry in %s/log/). If nothing durable was learned, finish normally."}\n' "$wiki" "$wiki"
 exit 0
diff --git a/test/test-install.sh b/test/test-install.sh
index 30586bd..cd08ba0 100755
--- a/test/test-install.sh
+++ b/test/test-install.sh
@@ -21,7 +21,9 @@ assert_file_contains "skill references wiki dir" "$repo/.claude/skills/wiki/SKIL
 # settings.json wiring
 s="$repo/.claude/settings.json"
 assert "settings.json created" test -f "$s"
-assert "settings valid json" python3 -c "import json;json.load(open('$s'))"
+assert "settings valid json" python3 -c "import json,sys;json.load(open(sys.argv[1]))" "$s"
+# bytes, not grep: Git for Windows' grep does not reliably see a trailing CR
+assert "settings.json written with LF on every platform" python3 -c "import sys; sys.exit(b'\r' in open(sys.argv[1], 'rb').read())" "$s"
 assert_file_contains "SessionStart wired" "$s" "wiki-session-start.sh"
 assert_file_contains "Stop wired" "$s" "wiki-stop-guard.sh"
 
@@ -43,7 +45,8 @@ assert "re-install preserves log entries" "$KIT_DIR/install.sh" "$repo"
 assert "custom log entry preserved" test -f "$repo/wiki/log/2026-09-08-record-custom.md"
 assert_grep_count "gitignore appended once" "$repo/.gitignore" ".obsidian/" 1
 assert_grep_count "CLAUDE.md pointer once" "$repo/CLAUDE.md" "wiki/WIKI.md" 1
-assert "settings hooks not duplicated" python3 -c "import json; h=json.load(open('$s'))['hooks']; assert len(h['SessionStart'])==1 and len(h['Stop'])==1"
+assert "settings hooks not duplicated" python3 -c "import json,sys; h=json.load(open(sys.argv[1]))['hooks']; assert len(h['SessionStart'])==1 and len(h['Stop'])==1" "$s"
+assert "settings hook commands not duplicated" python3 -c "import json,sys; h=json.load(open(sys.argv[1]))['hooks']; n=lambda ev,name: sum(name in x['command'] for e in h[ev] for x in e['hooks']); assert n('SessionStart','wiki-session-start.sh')==1 and n('Stop','wiki-stop-guard.sh')==1" "$s"
 
 # --- preserves unrelated settings + existing CLAUDE.md prose ---
 repo2=$(setup_repo)
@@ -85,7 +88,7 @@ printf '#!/bin/sh\nexit 127\n' > "$shim/python3" && chmod +x "$shim/python3"
 real_python3=$(command -v python3)
 printf '#!/bin/sh\nexec %s "$@"\n' "$real_python3" > "$shim/python" && chmod +x "$shim/python"
 assert "install falls back to python" env PATH="$shim:$PATH" "$KIT_DIR/install.sh" "$repo4"
-assert "settings valid json via fallback" python3 -c "import json;json.load(open('$repo4/.claude/settings.json'))"
+assert "settings valid json via fallback" python3 -c "import json,sys;json.load(open(sys.argv[1]))" "$repo4/.claude/settings.json"
 
 # --- python probe: no usable interpreter -> install fails loudly ---
 shim2=$(mktemp -d)
diff --git a/test/test-session-start.sh b/test/test-session-start.sh
index 769835b..fe4cbae 100755
--- a/test/test-session-start.sh
+++ b/test/test-session-start.sh
@@ -21,6 +21,12 @@ echo "---" > "$repo/wiki/systems/one.md"
 out=$(CLAUDE_PROJECT_DIR="$repo" "$hook")
 assert_str_contains "counts wiki pages" "$out" "(4 pages"   # WIKI, index, overview + one.md; raw/ and log/ excluded
 
+# a glob metacharacter in the project path must not disable the exclusions
+glob_parent="$(mktemp -d)/proj[1]"; mkdir -p "$glob_parent"
+cp -R "$repo/." "$glob_parent/"
+out=$(CLAUDE_PROJECT_DIR="$glob_parent" "$glob_parent/.claude/hooks/wiki-session-start.sh")
+assert_str_contains "page count survives a '[' in the project path" "$out" "(4 pages"
+
 # last update comes from the newest log entry filename
 echo "x" > "$repo/wiki/log/2099-01-01-record-future.md"
 out=$(CLAUDE_PROJECT_DIR="$repo" "$hook")
diff --git a/test/test-stop-guard.sh b/test/test-stop-guard.sh
index ed6bceb..ef3331a 100755
--- a/test/test-stop-guard.sh
+++ b/test/test-stop-guard.sh
@@ -81,4 +81,23 @@ out=$(printf '{"session_id":"probe2","stop_hook_active":false}' | env PATH="$shi
 assert "no-python exits 0" test $rc -eq 0
 assert_str_empty "no-python fails open silently" "$out"
 
+# a new wiki page whose name git quotes (non-ASCII) still counts as recorded
+repo_q=$(setup_repo)
+"$KIT_DIR/install.sh" "$repo_q" >/dev/null
+git -C "$repo_q" add -A >/dev/null && git -C "$repo_q" commit -qm "install kit"
+mkdir -p "$repo_q/src"; echo "code" > "$repo_q/src/app.py"
+qhook="$repo_q/.claude/hooks/wiki-stop-guard.sh"
+echo x > "$repo_q/wiki/notes/gyökeres.md"
+assert "git quotes the non-ASCII path (fixture precondition)" \
+  sh -c "git -C '$repo_q' status --porcelain | grep -qF '\"wiki/notes/'"
+out=$(printf '{"session_id":"q1","stop_hook_active":false}' | CLAUDE_PROJECT_DIR="$repo_q" TMPDIR="$tmp" "$qhook")
+assert_str_empty "quoted non-ASCII wiki path counts as recorded" "$out"
+
+# the prompt sends agents to the per-entry log, not the read-only legacy log.md
+rm "$repo_q/wiki/notes/gyökeres.md"
+out=$(printf '{"session_id":"q2","stop_hook_active":false}' | CLAUDE_PROJECT_DIR="$repo_q" TMPDIR="$tmp" "$qhook")
+assert_str_contains "prompt still blocks here" "$out" '"decision": "block"'
+assert_str_contains "prompt names the log directory" "$out" "wiki/log/"
+assert_str_not_contains "prompt does not name legacy log.md" "$out" "log.md"
+
 finish

Companion issues from the same install: #3 (the Stop guard keys on the tree, not the session) and #5 (smaller findings).

Field report from installing **f498b6f** on 2026-09-12 into a real, busy repository (Python app, ~1,500 tests, background jobs that write tracked data files, a 120 KB CLAUDE.md). **Environment:** Windows 11, Git for Windows (bash 5.3.9, `core.autocrlf=true` from the system gitconfig), Python 3.14 (`python3` is the WindowsApps alias and works), Claude Code 2.1.269 → 2.1.270. **How it was checked:** the kit's own suite; the hooks driven exactly as `settings.json` holds them; and four live headless `claude -p` sessions reading the `hook_response` events. **Nothing here was run on Linux or macOS.** ## Fixed in the patch below Kit suite: **90 passed / 3 failed → 101 passed / 0 failed** on Windows. Every new assertion was proven RED against unpatched f498b6f code before being trusted. ### A1. Three `test-install.sh` assertions can never pass on Windows `test-install.sh:24, :46, :88` embed the settings path inside the Python source: ```sh python3 -c "import json;json.load(open('$s'))" ``` MSYS converts paths in **argv** for native executables, but not paths inside a string, so native Python receives `/tmp/tmp.XXXX/...` and raises `FileNotFoundError`. The installer itself is fine: it passes the path as argv. The consequence is worse than a red suite. These three stay red *identically* when the settings merge is genuinely broken (duplicated hooks, invalid JSON), so on Windows they cannot detect the regressions they exist for. **Fix:** pass the path as `sys.argv[1]`. **Related:** the "hooks not duplicated" assertion counts *entries*, not *commands*. A merge that nests the same command three times inside one entry stays green (mutation-tested). The patch adds a per-command count. ### A2. The Stop prompt sends agents to `log.md`, which the contract calls read-only `wiki-stop-guard.sh` says `(update page, crosslinks, index.md, log.md)`, while `WIKI.md` has said since e8f0e76 that *"A log.md from older installs is read-only history"*. The log-per-entry migration missed this line. **Fix:** the prompt now reads `a new entry in <wiki>/log/`, with tests that it names `wiki/log/` and not `log.md`. ### A3. The SessionStart page count uses the project path as a glob ```sh find "$dir" -name '*.md' -not -path "$dir/raw/*" -not -path "$dir/log/*" ``` `-path` takes a pattern, so a `[` or a backslash in the absolute project path makes both exclusions match nothing. Log entries and raw sources are then counted as pages, with no error. Reproduced two ways: - `CLAUDE_PROJECT_DIR` given as a backslash path (`cygpath -w`): the hook reports 4 pages instead of 3. - A forward-slash path containing `proj[1]`: same result. It is latent on Claude Code 2.1.269, which exports forward slashes. Any user whose path contains brackets gets a count that grows with every log entry. **Fix:** `cd "$dir"`, then use relative patterns. A new test installs into a directory named `proj[1]`. ### A4. The Stop guard misreads quoted porcelain paths The guard classifies inside/outside with `substr($0, 4, ...)` over `git status --porcelain`. Git quotes paths containing spaces or non-ASCII bytes (`?? "wiki/notes/gy\303\266keres.md"`). The column-4 match then fails, a new wiki page counts as *outside* the wiki, and the agent is told it recorded nothing right after recording something. **Fix:** two pathspec-limited status calls: - outside: `-- ':(top)' ":(exclude)$wiki"` - inside: `-- "$wiki"` The existing subdirectory-rooted tests still pass. A new test uses a non-ASCII page name, with a precondition asserting that git really quotes it. ### A5. The installer writes `settings.json` with CRLF on Windows `open(path, "w")` is text mode. **Fix:** `newline="\n"`. The test checks bytes, because Git for Windows' `grep` does not reliably see a trailing CR: the first `grep $'\r'` version of this assertion could not fail. ## The patch (applies to f498b6f, 134 lines) ```diff diff --git a/install.sh b/install.sh index 7d8cd7d..801b1e0 100755 --- a/install.sh +++ b/install.sh @@ -93,7 +93,7 @@ for event, cmd in wanted.items(): ) if not present: entries.append({"hooks": [{"type": "command", "command": cmd}]}) -with open(path, "w") as f: +with open(path, "w", newline="\n") as f: json.dump(settings, f, indent=2) f.write("\n") PYEOF diff --git a/template/claude/hooks/wiki-session-start.sh b/template/claude/hooks/wiki-session-start.sh index 0d2d7b1..498ed1b 100644 --- a/template/claude/hooks/wiki-session-start.sh +++ b/template/claude/hooks/wiki-session-start.sh @@ -3,7 +3,9 @@ # stdout is injected into the session's context. dir="${CLAUDE_PROJECT_DIR:-.}/{{WIKI_DIR}}" [ -d "$dir" ] || exit 0 -count=$(find "$dir" -name '*.md' -not -path "$dir/raw/*" -not -path "$dir/log/*" 2>/dev/null | wc -l | tr -d ' ') +# Patterns relative to the wiki dir: -path reads its argument as a glob, so an +# absolute $dir containing '[' or a backslash silently disables the exclusions. +count=$(cd "$dir" 2>/dev/null && find . -name '*.md' -not -path './raw/*' -not -path './log/*' | wc -l | tr -d ' ') last=$(ls "$dir/log/" 2>/dev/null | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}-' | sort | tail -n 1 | cut -c1-10) [ -n "$last" ] || last=$(grep -E '^- [0-9]{4}-' "$dir/log.md" 2>/dev/null | tail -n 1 | awk '{print $2}') cat <<EOF diff --git a/template/claude/hooks/wiki-stop-guard.sh b/template/claude/hooks/wiki-stop-guard.sh index f5ca171..6b4bc73 100644 --- a/template/claude/hooks/wiki-stop-guard.sh +++ b/template/claude/hooks/wiki-stop-guard.sh @@ -33,15 +33,15 @@ marker="${TMPDIR:-/tmp}/wiki-stop-guard-${session_id}" [ -e "$marker" ] && exit 0 [ -d "$proj/$wiki" ] || exit 0 git -C "$proj" rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0 -prefix=$(git -C "$proj" rev-parse --show-prefix 2>/dev/null) || exit 0 - -status=$(git -C "$proj" status --porcelain 2>/dev/null) || exit 0 -# porcelain: 2 status chars + space, path starts at column 4 -outside=$(printf '%s\n' "$status" | awk -v d="${prefix}${wiki}/" 'NF && substr($0,4,length(d)) != d' | head -n 1) +# Pathspecs, not column parsing: porcelain quotes paths containing spaces or +# non-ASCII bytes, and a substr() on column 4 reads those as outside the wiki. +# ':(top)' keeps "anything in the repo" for subdirectory-rooted projects; the +# wiki pathspecs resolve relative to $proj. +outside=$(git -C "$proj" status --porcelain -- ':(top)' ":(exclude)$wiki" 2>/dev/null) || exit 0 [ -n "$outside" ] || exit 0 -inside=$(printf '%s\n' "$status" | awk -v d="${prefix}${wiki}/" 'substr($0,4,length(d)) == d' | head -n 1) +inside=$(git -C "$proj" status --porcelain -- "$wiki" 2>/dev/null) || exit 0 [ -n "$inside" ] && exit 0 touch "$marker" 2>/dev/null -printf '{"decision": "block", "reason": "Before finishing: this session changed files but recorded nothing in the wiki. If you learned something durable the next agent could not cheaply re-derive, record it now per %s/WIKI.md (update page, crosslinks, index.md, log.md). If nothing durable was learned, finish normally."}\n' "$wiki" +printf '{"decision": "block", "reason": "Before finishing: this session changed files but recorded nothing in the wiki. If you learned something durable the next agent could not cheaply re-derive, record it now per %s/WIKI.md (update page, crosslinks, index.md, a new entry in %s/log/). If nothing durable was learned, finish normally."}\n' "$wiki" "$wiki" exit 0 diff --git a/test/test-install.sh b/test/test-install.sh index 30586bd..cd08ba0 100755 --- a/test/test-install.sh +++ b/test/test-install.sh @@ -21,7 +21,9 @@ assert_file_contains "skill references wiki dir" "$repo/.claude/skills/wiki/SKIL # settings.json wiring s="$repo/.claude/settings.json" assert "settings.json created" test -f "$s" -assert "settings valid json" python3 -c "import json;json.load(open('$s'))" +assert "settings valid json" python3 -c "import json,sys;json.load(open(sys.argv[1]))" "$s" +# bytes, not grep: Git for Windows' grep does not reliably see a trailing CR +assert "settings.json written with LF on every platform" python3 -c "import sys; sys.exit(b'\r' in open(sys.argv[1], 'rb').read())" "$s" assert_file_contains "SessionStart wired" "$s" "wiki-session-start.sh" assert_file_contains "Stop wired" "$s" "wiki-stop-guard.sh" @@ -43,7 +45,8 @@ assert "re-install preserves log entries" "$KIT_DIR/install.sh" "$repo" assert "custom log entry preserved" test -f "$repo/wiki/log/2026-09-08-record-custom.md" assert_grep_count "gitignore appended once" "$repo/.gitignore" ".obsidian/" 1 assert_grep_count "CLAUDE.md pointer once" "$repo/CLAUDE.md" "wiki/WIKI.md" 1 -assert "settings hooks not duplicated" python3 -c "import json; h=json.load(open('$s'))['hooks']; assert len(h['SessionStart'])==1 and len(h['Stop'])==1" +assert "settings hooks not duplicated" python3 -c "import json,sys; h=json.load(open(sys.argv[1]))['hooks']; assert len(h['SessionStart'])==1 and len(h['Stop'])==1" "$s" +assert "settings hook commands not duplicated" python3 -c "import json,sys; h=json.load(open(sys.argv[1]))['hooks']; n=lambda ev,name: sum(name in x['command'] for e in h[ev] for x in e['hooks']); assert n('SessionStart','wiki-session-start.sh')==1 and n('Stop','wiki-stop-guard.sh')==1" "$s" # --- preserves unrelated settings + existing CLAUDE.md prose --- repo2=$(setup_repo) @@ -85,7 +88,7 @@ printf '#!/bin/sh\nexit 127\n' > "$shim/python3" && chmod +x "$shim/python3" real_python3=$(command -v python3) printf '#!/bin/sh\nexec %s "$@"\n' "$real_python3" > "$shim/python" && chmod +x "$shim/python" assert "install falls back to python" env PATH="$shim:$PATH" "$KIT_DIR/install.sh" "$repo4" -assert "settings valid json via fallback" python3 -c "import json;json.load(open('$repo4/.claude/settings.json'))" +assert "settings valid json via fallback" python3 -c "import json,sys;json.load(open(sys.argv[1]))" "$repo4/.claude/settings.json" # --- python probe: no usable interpreter -> install fails loudly --- shim2=$(mktemp -d) diff --git a/test/test-session-start.sh b/test/test-session-start.sh index 769835b..fe4cbae 100755 --- a/test/test-session-start.sh +++ b/test/test-session-start.sh @@ -21,6 +21,12 @@ echo "---" > "$repo/wiki/systems/one.md" out=$(CLAUDE_PROJECT_DIR="$repo" "$hook") assert_str_contains "counts wiki pages" "$out" "(4 pages" # WIKI, index, overview + one.md; raw/ and log/ excluded +# a glob metacharacter in the project path must not disable the exclusions +glob_parent="$(mktemp -d)/proj[1]"; mkdir -p "$glob_parent" +cp -R "$repo/." "$glob_parent/" +out=$(CLAUDE_PROJECT_DIR="$glob_parent" "$glob_parent/.claude/hooks/wiki-session-start.sh") +assert_str_contains "page count survives a '[' in the project path" "$out" "(4 pages" + # last update comes from the newest log entry filename echo "x" > "$repo/wiki/log/2099-01-01-record-future.md" out=$(CLAUDE_PROJECT_DIR="$repo" "$hook") diff --git a/test/test-stop-guard.sh b/test/test-stop-guard.sh index ed6bceb..ef3331a 100755 --- a/test/test-stop-guard.sh +++ b/test/test-stop-guard.sh @@ -81,4 +81,23 @@ out=$(printf '{"session_id":"probe2","stop_hook_active":false}' | env PATH="$shi assert "no-python exits 0" test $rc -eq 0 assert_str_empty "no-python fails open silently" "$out" +# a new wiki page whose name git quotes (non-ASCII) still counts as recorded +repo_q=$(setup_repo) +"$KIT_DIR/install.sh" "$repo_q" >/dev/null +git -C "$repo_q" add -A >/dev/null && git -C "$repo_q" commit -qm "install kit" +mkdir -p "$repo_q/src"; echo "code" > "$repo_q/src/app.py" +qhook="$repo_q/.claude/hooks/wiki-stop-guard.sh" +echo x > "$repo_q/wiki/notes/gyökeres.md" +assert "git quotes the non-ASCII path (fixture precondition)" \ + sh -c "git -C '$repo_q' status --porcelain | grep -qF '\"wiki/notes/'" +out=$(printf '{"session_id":"q1","stop_hook_active":false}' | CLAUDE_PROJECT_DIR="$repo_q" TMPDIR="$tmp" "$qhook") +assert_str_empty "quoted non-ASCII wiki path counts as recorded" "$out" + +# the prompt sends agents to the per-entry log, not the read-only legacy log.md +rm "$repo_q/wiki/notes/gyökeres.md" +out=$(printf '{"session_id":"q2","stop_hook_active":false}' | CLAUDE_PROJECT_DIR="$repo_q" TMPDIR="$tmp" "$qhook") +assert_str_contains "prompt still blocks here" "$out" '"decision": "block"' +assert_str_contains "prompt names the log directory" "$out" "wiki/log/" +assert_str_not_contains "prompt does not name legacy log.md" "$out" "log.md" + finish ``` Companion issues from the same install: #3 (the Stop guard keys on the tree, not the session) and #5 (smaller findings).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kristofferopsahl/auto-llm-wiki#4
No description provided.