Skip to content

Commit 9062c73

Browse files
authored
Merge pull request #38 from malkreide/claude/session-start-hook-72c9ni
feat: SessionStart-Hook meldet Rückstand hinter origin/<Default-Branch>
2 parents 2529cf8 + bf2e656 commit 9062c73

5 files changed

Lines changed: 508 additions & 0 deletions

File tree

.claude/hooks/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# SessionStart-Hook: Klon-Aktualität
2+
3+
`session-start.sh` meldet beim Sessionstart, wie viele Commits der
4+
ausgecheckte Stand hinter `origin/<Default-Branch>` liegt. Registriert ist er
5+
in `.claude/settings.json` für die Quellen `startup` und `resume` — nicht für
6+
`compact`, das während einer Session mehrfach feuert und dabei nichts Neues
7+
prüfen würde.
8+
9+
## Warum es ihn gibt
10+
11+
Ein veralteter Klon hat am 3.8.2026 zweimal eine rote CI erzeugt, deren
12+
Ursache nicht im Diff stand. Die fehlenden Commits waren jeweils genau die,
13+
die das Gate einführten, an dem der Branch scheiterte — der Fehler zeigte auf
14+
Dateien, die niemand angefasst hatte. Die Prüfung kostet eine Sekunde und
15+
ersetzt eine Fehlersuche in den falschen Dateien.
16+
17+
Die Prüfung stand bis dahin nur als Merksatz in `CLAUDE.md`, ganz oben unter
18+
«Vor der Arbeit». Ein Merksatz wirkt genau so lange, wie ihn jemand liest.
19+
20+
## Was er zusichert
21+
22+
1. **Er blockiert die Session nie.** Kein Netz, kein Remote `origin`, kein
23+
Git-Repo, detached HEAD, ein Repo ohne Commits, flatterndes DNS, fehlende
24+
Anmeldedaten — jeder dieser Fälle endet still mit Exit 0 und ohne Ausgabe.
25+
Das ist die oberste Regel, wichtiger als jede Meldung: Ein Hook, der bei
26+
Netzproblemen die Arbeit anhält, wird nach dem zweiten Mal abgeschaltet und
27+
schützt danach gar nichts mehr.
28+
2. **Kurzes Timeout aufs Netz.** `timeout(1)` deckelt jeden Git-Aufruf auf
29+
8 Sekunden (`CLAUDE_STALE_CHECK_TIMEOUT` übersteuert das). Fehlt `timeout`
30+
auf dem System, greifen `GIT_HTTP_LOW_SPEED_LIMIT`/`_TIME` als zweite
31+
Reissleine. Zusätzlich sind alle Anmeldedialoge abgeschaltet
32+
(`GIT_TERMINAL_PROMPT=0`, `GIT_ASKPASS`, `ssh -o BatchMode=yes`): Ein
33+
wartender Passwort-Prompt hängt den Sessionstart, ohne dass ein Timeout auf
34+
`git` überhaupt greifen würde — der Prozess läuft ja, er wartet nur.
35+
Zusätzlich deckelt `settings.json` den Hook selbst auf 15 Sekunden.
36+
3. **Ausgabe nur, wenn Commits fehlen.** Bei 0 schweigt er. Eine Meldung
37+
«alles aktuell» bei jedem Sessionstart wäre genau das Rauschen, das man
38+
nach der dritten Woche nicht mehr liest.
39+
4. **Der Default-Branch wird ermittelt, nicht angenommen.** Zuerst lokal über
40+
`refs/remotes/origin/HEAD` (kein Netz), sonst über `git ls-remote --symref`.
41+
Liefert beides nichts, wird nichts geraten und der Hook schweigt. Drei
42+
Server im Portfolio (`openlex-mcp`, `swiss-courts-mcp`, `swisstopo-mcp`)
43+
heissen ihren Default-Branch `master`; ein fest verdrahtetes `origin/main`
44+
scheitert dort mit «couldn't find remote ref main». Wer das für ein
45+
Netzproblem hält, arbeitet weiter auf genau dem veralteten Klon — so wurde
46+
ein Branch einmal 15 Commits alt.
47+
48+
`tests/test_session_start_hook.py` hält diese vier Punkte fest und führt das
49+
Skript dafür gegen echte Wegwerf-Repositorien aus.
50+
51+
## Selbst ausprobieren
52+
53+
```bash
54+
CLAUDE_PROJECT_DIR="$PWD" .claude/hooks/session-start.sh; echo "Exit: $?"
55+
```
56+
57+
Auf einem aktuellen Klon: keine Ausgabe, Exit 0.

.claude/hooks/session-start.sh

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env bash
2+
# SessionStart-Hook: meldet, wie viele Commits der ausgecheckte Stand hinter
3+
# origin/<Default-Branch> liegt. Begruendung: .claude/hooks/README.md
4+
#
5+
# Oberste Regel, wichtiger als jede Meldung: Dieser Hook blockiert die Session
6+
# nie. Kein Netz, kein Remote, detached HEAD, flatterndes DNS, fehlende
7+
# Anmeldedaten — jeder dieser Faelle endet still mit Exit 0 und ohne Ausgabe.
8+
# Ein Hook, der bei Netzproblemen die Arbeit anhaelt, wird nach dem zweiten Mal
9+
# abgeschaltet und schuetzt danach gar nichts mehr.
10+
11+
# Kein `set -e`: Ein fehlschlagender Befehl ist hier der Normalfall (offline),
12+
# kein Abbruchgrund. Das `trap` faengt ab, was trotzdem durchrutscht — etwa
13+
# eine ungesetzte Variable unter `set -u`.
14+
set -uo pipefail
15+
trap 'exit 0' EXIT
16+
17+
# Sekunden, die das Netz insgesamt kosten darf.
18+
FETCH_TIMEOUT=${CLAUDE_STALE_CHECK_TIMEOUT:-8}
19+
20+
# Ein wartender Anmeldedialog haengt den Sessionstart, ohne dass ein Timeout
21+
# auf `git` ueberhaupt greift: Der Prozess laeuft ja, er wartet nur auf eine
22+
# Eingabe, die niemand gibt. Deshalb alle Dialoge vorab abschalten.
23+
export GIT_TERMINAL_PROMPT=0
24+
export GIT_ASKPASS=/bin/true
25+
export SSH_ASKPASS=/bin/true
26+
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o BatchMode=yes -o ConnectTimeout=5}"
27+
# Zweite Reissleine fuer den Fall, dass `timeout(1)` fehlt: git bricht eine
28+
# HTTP-Uebertragung ab, die so lange fast nichts mehr liefert.
29+
export GIT_HTTP_LOW_SPEED_LIMIT=1000
30+
export GIT_HTTP_LOW_SPEED_TIME="$FETCH_TIMEOUT"
31+
32+
mit_timeout() {
33+
if command -v timeout >/dev/null 2>&1; then
34+
timeout "$FETCH_TIMEOUT" "$@"
35+
else
36+
"$@"
37+
fi
38+
}
39+
40+
# Den Default-Branch ermitteln, nicht `main` annehmen: Drei Server im
41+
# Portfolio (openlex-mcp, swiss-courts-mcp, swisstopo-mcp) heissen ihren
42+
# `master`. Genau diese Annahme hat schon einmal einen Branch 15 Commits alt
43+
# werden lassen, weil `origin/main` mit «couldn't find remote ref main»
44+
# scheiterte und das wie ein Netzproblem aussah.
45+
default_branch() {
46+
local zweig
47+
# Zuerst lokal, ohne Netz: `git clone` setzt diese Referenz.
48+
zweig=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null)
49+
zweig=${zweig#origin/}
50+
if [ -n "$zweig" ]; then
51+
printf '%s\n' "$zweig"
52+
return 0
53+
fi
54+
# Sonst den Remote fragen. Schlaegt das fehl, wird nichts geraten.
55+
mit_timeout git ls-remote --symref origin HEAD 2>/dev/null |
56+
sed -n 's|^ref: refs/heads/\([^[:space:]]*\).*|\1|p' |
57+
head -n 1
58+
}
59+
60+
cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || exit 0
61+
62+
git rev-parse --git-dir >/dev/null 2>&1 || exit 0 # kein Git-Repo
63+
git rev-parse --verify --quiet HEAD >/dev/null 2>&1 || exit 0 # noch kein Commit
64+
git remote get-url origin >/dev/null 2>&1 || exit 0 # kein Remote `origin`
65+
66+
ZWEIG=$(default_branch)
67+
[ -n "${ZWEIG:-}" ] || exit 0
68+
69+
# Nur dieser eine Branch, kein `--tags`, kein `--all`: Der Hook soll billig
70+
# sein. Der Aufruf aktualisiert FETCH_HEAD und die Remote-Tracking-Referenz,
71+
# er veraendert den Arbeitsbaum nicht.
72+
mit_timeout git fetch --quiet origin "$ZWEIG" >/dev/null 2>&1 || exit 0
73+
74+
ZIEL=$(git rev-parse --verify --quiet FETCH_HEAD 2>/dev/null)
75+
[ -n "${ZIEL:-}" ] || exit 0
76+
77+
# Funktioniert auch bei detached HEAD — dort ist HEAD eine gueltige Revision
78+
# wie jede andere.
79+
HINTER=$(git rev-list --count "HEAD..$ZIEL" 2>/dev/null)
80+
case "${HINTER:-}" in
81+
'' | *[!0-9]*) exit 0 ;;
82+
esac
83+
84+
# Bei 0 schweigt der Hook. Eine Meldung «alles aktuell» bei jedem Sessionstart
85+
# waere genau das Rauschen, das man nach der dritten Woche nicht mehr liest.
86+
[ "$HINTER" -gt 0 ] || exit 0
87+
88+
if [ "$HINTER" -eq 1 ]; then
89+
WORT="Commit"
90+
else
91+
WORT="Commits"
92+
fi
93+
94+
cat <<MELDUNG
95+
[Klon-Aktualitaet] Der ausgecheckte Stand liegt $HINTER $WORT hinter origin/$ZWEIG.
96+
97+
Ein veralteter Klon erzeugt eine rote CI, deren Ursache nicht im Diff steht:
98+
Die fehlenden Commits sind erfahrungsgemaess genau die, die das Gate
99+
einfuehren, an dem der Branch dann scheitert. Vor dem Arbeiten aktualisieren:
100+
101+
git fetch origin $ZWEIG && git merge FETCH_HEAD
102+
MELDUNG
103+
104+
exit 0

.claude/settings.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"hooks": {
3+
"SessionStart": [
4+
{
5+
"matcher": "startup|resume",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh",
10+
"timeout": 15
11+
}
12+
]
13+
}
14+
]
15+
}
16+
}

CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ den Remote-HEAD und endet mit 0.
2020
Ein veralteter Klon erzeugt eine rote CI, deren Ursache nicht im Diff steht.
2121
Am 3.8.2026 zweimal passiert — beide Male fehlten genau die Commits, die
2222
das Gate einführten, an dem der Branch scheiterte.
23+
24+
Seit `.claude/settings.json` läuft diese Prüfung als SessionStart-Hook
25+
(`.claude/hooks/session-start.sh`) und meldet den Rückstand von selbst. Sie
26+
blockiert nie und schweigt bei 0 — bleibt oben also von Hand zu fahren, wenn
27+
der Hook nicht greift (fremder Klon, kein Netz beim Start). Begründung und
28+
Zusicherungen: `.claude/hooks/README.md`.
29+
2330
Gates lokal fahren, mit der GEPINNTEN ruff-Version aus der CI. Eine andere
2431
Version meldet Abweichungen, die niemand verursacht hat.
2532

0 commit comments

Comments
 (0)