Skip to content

fix(windows): CLAUDE_PID names the window before the ancestor walk (#835, slice 1) - #909

Merged
github-actions[bot] merged 2 commits into
mainfrom
fix/835-claude-pid-window-identity
Nov 6, 2026
Merged

github-actions[bot] merged 2 commits into
mainfrom
fix/835-claude-pid-window-identity

Conversation

@cdeust

@cdeust cdeust commented Nov 6, 2026

Copy link
Copy Markdown
Owner

Summary

Sous Windows, le hook d'une fenêtre retrouve son claude.exe même quand un bash.exe intermédiaire est déjà sorti. C'est la première tranche de #835 (ADR-1124, qui amende le point 4 d'ADR-1096).

Cause racine. process_ancestry.claude_ancestor_pid remonte la table des processus parent par parent. Dans un vrai hook, la commande bash -c '… "$PY" …' peut faire un exec, et le bash.exe intermédiaire sort (chaîne relevée par le rapporteur le 2026-10-07 : python.exe <- bash.exe <- <pid sorti>). La marche s'arrête alors sur le parent manquant (process_ancestry.py, if parent is None …: return None, le cas épinglé par test_parent_missing_from_table_returns_none). Ensuite groomer_identity.window_pid lève WindowIdentityUnavailableError, SessionStart ne lance aucun cycle de consolidation (ADR-1097) et le registre de la fenêtre n'est pas écrit.

Correctif. Sous Windows, session_registry.find_claude_ancestor appelle le nouveau process_ancestry.window_claude_pid. Celui-ci lit la table une seule fois et renvoie CLAUDE_PID si quatre conditions sont réunies : la valeur est un entier, la table contient ce pid, son exécutable est claude.exe (casse ignorée), et sa date de création est lisible et antérieure ou égale à celle de l'appelant (un pid recyclé est plus jeune que l'appelant). Sinon, la marche d'ADR-1096 répond, sur la même lecture de table. La marche elle-même ne change pas : elle est extraite telle quelle dans _walk. POSIX ne change pas non plus.

Refs #835 (première tranche ; les tranches restantes sont listées dans le commentaire de l'issue).

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Test plan

  • tests_py/infrastructure/test_process_ancestry_claude_pid.py (nouveau, 9 tests sur une table de processus factice) : la chaîne cassée de Windows: the ADR-1096 ancestor walk returns None in real hooks when an intermediate bash has exited; CLAUDE_PID identifies the window #835 est résolue par CLAUDE_PID ; les cas où CLAUDE_PID est absent, non entier, désigne un autre programme, manque dans la table, est plus jeune que l'appelant ou a une date de création illisible ; la casse ; une seule lecture de table.
  • test_hook_with_an_exited_bash_finds_the_window_through_claude_pid dans test_session_registry_windows.py : de bout en bout par find_claude_ancestor. La fixture retire le CLAUDE_PID de l'hôte, qui ne doit pas influencer les tests.
  • tests_py/infrastructure/test_session_registry_posix_root.py (nouveau, 2 tests) : la borne ppid <= 1 de la marche POSIX. Elle n'était pas testée, et le gate de mutation la met dans le périmètre parce que find_claude_ancestor change.
  • pytest tests_py/infrastructure -k "ancestry or session_registry" : 73 passés, 19 ignorés.
  • pytest tests_py/infrastructure tests_py/hooks tests_py/shared : 2937 passés, 5 échecs. Ces 5 échecs (test_embedding_engine.py ×3, test_entry_auto_dispatch.py ×2) se reproduisent à l'identique sur origin/main sans ce diff : sentence-transformers et torch n'ont pas pu être installés ici (erreur de tunnel du proxy).
  • Gate de mutation ADR-1122 lancé localement avec la même planification que la CI (scripts/ci_mutation_diff.py plan puis run_job, mutmut 3.8.1 du lock) : process_ancestry.py 97 mutants, RC 0 ; session_registry.py, RC 0. Une première passe avait laissé un mutant équivalent ("" → "XXXX", deux ValueError), que j'ai supprimé en réécrivant la lecture de CLAUDE_PID, et deux mutants survivants de la borne POSIX, tués par les tests ci-dessus.
  • ruff check : passe sur les fichiers modifiés. Sur l'ensemble du dépôt, il signale 8 E501 dans scripts/ et tests_py/scripts/, tous déjà présents sur origin/main. ruff format --check passe sur les fichiers modifiés. python scripts/check_craftsmanship.py : OK. python scripts/check_project_wiki.py : les miroirs correspondent.

Audit notes

  • Non vérifié : aucun hôte Windows réel ni vrai hook dans cette session. L'existence et la valeur de CLAUDE_PID reposent sur la mesure du rapporteur (Claude Code 2.1.289 à 2.1.309 ; 879 recherches sur 892 du 2026-10-21 au 2026-10-29). La variable n'est pas documentée par l'hôte. Si elle disparaît ou change de sens, le contrôle la rejette et le comportement redevient celui de la marche d'ADR-1096, comme avant ce changement.
  • Limites restantes, écrites dans ADR-1124 : un CLAUDE_PID qui désignerait le Claude.exe partagé du bureau serait accepté (même limite que le point 7 d'ADR-1096). Un pid recyclé par un autre claude.exe entre la sortie du claude.exe de la fenêtre et la création de l'appelant passerait aussi le contrôle.
  • Hors de cette tranche : le SessionEnd d'une fenêtre dont le claude.exe est déjà sorti, et les sessions de terminal sans CLAUDE_PID.

Coding-standards compliance

Reviewer checklist

  • CHANGELOG.md mis à jour (Unreleased → Fixed).
  • Documentation : ADR-1124 (page wiki canonique, miroir docs/adr/, wiki/manifest.json).
  • Aucun secret, identifiant ni donnée personnelle dans le diff.
  • CI : pas encore de résultat sur ce commit.

Generated by Claude Code

, slice 1)

In a real hook the ADR-1096 walk can meet an intermediate bash.exe that has
already exited: claude_ancestor_pid returns None, window_pid raises, and
SessionStart starts no consolidate cycle. On Windows the reader now tries
CLAUDE_PID first, used only when it names a claude.exe row not younger than
the caller, over the same single table read; otherwise the walk answers.

ADR-1124 amends ADR-1096 point 4.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYsD6hWSSvhEvcoANQR
@cdeust
cdeust marked this pull request as ready for review November 6, 2026 17:50
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T17:51:34.736738Z c436707 Draft marked ready
🔒 Security Review ✅ Completed 2026-11-06T17:53:55.251346Z c436707 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Claude finished @cdeust's task in 3m 39s —— View job


PR #909 review, round 1 of 3

  • Read the diff
  • Ran five reviews: code quality, documentation, performance, security, test coverage
  • Run tests and lint locally: not permitted in this job (uv/pytest need approval). CI "Pipeline" was still running when this was posted.
  • Posted the summary here. gh pr comment was also not permitted, so this comment is the review.

No Blockers. What I checked:

  • Extracting _walk from claude_ancestor_pid does not change the walk's behaviour.
  • The four checks on CLAUDE_PID (integer, row present, claude.exe in any case, creation time not after the caller's) match ADR-1124 and the docstrings.
  • The table is read once.
  • The pid only reaches a file path as an int (registry_path), so it cannot be used for path traversal.
  • Layering, the 300/40 line limits and # source: comments are fine.
  • No other test can pick up the host's CLAUDE_PID.
  • In the common case the change opens fewer process handles than the old walk.

Notes, no action before merge

  1. In process_ancestry.py:144-147 the CLAUDE_PID check runs before the walk and never checks that the pid is an ancestor of the caller. Suppose a claude.exe B is started inside window A (the claude command run from the Bash tool, or an SDK subprocess) and inherits CLAUDE_PID=A. B's hooks then resolve to A, even though the walk would correctly return B. B would overwrite A.json, its SessionEnd would tombstone A's entry, and its stop_if_last would run against A. This depends on whether a nested claude.exe sets CLAUDE_PID itself, which is not verified. Running the walk first and using CLAUDE_PID only when it returns None would still fix Windows: the ADR-1096 ancestor walk returns None in real hooks when an intermediate bash has exited; CLAUDE_PID identifies the window #835 and close this gap. A test where the walk and CLAUDE_PID give different answers would then pin the order. Otherwise, add this case to ADR-1124's Consequences. Fix this →
  2. claude_ancestor_pid no longer has a production caller; only tests use it. ADR-1097 point 1 and the first sentence of the find_claude_ancestor docstring ("walk from os.getppid()… nearest claude") are now out of date on Windows.
  3. When CLAUDE_PID is present but rejected and the walk then fails, WindowIdentityUnavailableError gives no sign that the variable existed. A debug log line with the rejection reason would make Windows: the ADR-1096 ancestor walk returns None in real hooks when an intermediate bash has exited; CLAUDE_PID identifies the window #835-style reports easier to diagnose.

Verdict: approve

@github-actions
github-actions Bot merged commit cf907bd into main Nov 6, 2026
21 of 28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants