Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 51 additions & 15 deletions docs/decisions/2026-09-28-terminal-experience.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/harness-defaults.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,7 @@ When a claim or action proves wrong, correct it where it was relayed and record
| 2026-09-30 | Repairing a cross-family review's findings without grading them | Of the 23 findings a verifier graded, 4 were narrower than claimed and 2 were wrong: identical command hooks run once in Claude Code, and a terminal's permission notification is timed by inactivity (the reviewer had read the Agent SDK path). Applying every finding would have added a false rule and a wrong documentation change | Grade each finding with a read-only verifier, or against the code or the upstream source, before repairing it; record refutations in the same table | `evidence/artifacts/terminal-lane-cross-family-review-20260930/` (`findings.json`, `verification.json`) |
| 2026-09-30 | Correcting a wrong statement with another unverified statement | The first repair of a documentation line about the permission prompt called the fixed-delay timer in the binary the terminal dialog's notifier. The function's only callers were the Agent SDK's; the terminal path compares the time with the last interaction. A verifier caught it after the wording was in three documents and a receipt | Verify a correction like a first claim: follow the call path from the user-visible behaviour to the code instead of taking the first function with the right name | `evidence/artifacts/terminal-lane-cross-family-review-20260930/` (row `F-sol-u519-A1-3`), decision record 2026-09-28 (last section) |
| 2026-09-30 | Claiming a concurrency or signal guarantee that no case exercises, and hardening with a mechanism whose side effect is untested | The settings-swap tool's docstring said the backup holds a save that lands after its last comparison (it is in neither file), and the probes' first signal-handling repair blocked signals around the client's creation, which a client inherits across `exec` (a `sleep` child shows `SigBlk` 0x4003, so its `SIGTERM` is blocked). Both passed their own controls and were found by a second review and its verifiers | State such a guarantee only with one case per interleaving that injects the event at that point and requires a marker that the injection happened; check a mechanism's effect on the processes it spawns; give every guard a mutant that shows its case failing without it | `evidence/artifacts/notification-types-20260929/` (`concurrent_writer_harness.py`, `replace_bell_group_controls.py`), `evidence/artifacts/terminal-experience-20260928/pty_probe_mutants.py` |
| 2026-09-30 | Raising an exception from a signal handler to run the cleanup | The pty probes' handler raised to unwind into `finally`. Python's `signal` documentation says such an exception "may be raised after any bytecode instruction" (docs.python.org/3/library/signal.html), so a signal could land between the creation of the private directory and the `try` that removes it, or between a spawn and its registration, and a second, different signal could replace the first. The re-check's repairs (a signal mask, a spawn flag) moved the windows without closing them; the final read found it | A handler only records the first signal and returns; the code acts on the latch at explicit safe points, always finishes its cleanups and exits with 128 plus the signal afterwards; test it by delivering the signal at every line of the resource lifecycle (a traced dry run lists the lines) and give each guard a mutant | `evidence/artifacts/terminal-experience-20260928/alert_probe.py` (`--selftest`, `sweep_case`), `evidence/artifacts/notification-types-20260929/push_notification_probe.py`, `evidence/artifacts/terminal-experience-20260928/pty_probe_mutants.py` |
| 2026-09-30 | Judging a shutdown by an exit status that cannot tell the cases apart | The tmux probes ran `kill-server` and removed their socket directory whatever it returned. Its status is nonzero both for a server that stopped and for one that was never there, so a surviving server could keep running with its socket gone; the first repair still relied on the status | Check the resource itself: identify the process by pid, start time and a command line that holds the private socket path, signal that identity when the polite stop fails, and keep the directory and fail when it cannot be identified or stopped; give the check a control whose stop command refuses and one whose identity lies | `evidence/artifacts/notification-types-20260929/tmux_bell_probe.py`, `evidence/artifacts/terminal-lane-cross-family-review-20260930/tmux_sync_probe.py` (`shutdown_control`, run by each `--selftest`) |
| 2026-09-30 | Reading upstream syntax with a shorter rule than its grammar | The notification-type scan took any assignment to the catalog's name (no complete initializer, member access through whitespace or a comment, a second unreadable catalog ignored), the landscape comparison read one `Default` impl and a struct's text, and the colour counter classified SGR groups by a pattern instead of the pinned parser's rules (saturation at 65535, missing components as 0, byte bounds, the colon form's colour-space field). Each returned a confident answer that the source would not give | Emulate the pinned parser's own rules and cite its file and tag, test the reader on the real upstream files of two releases and on the mutations an upstream change would plausibly make, and make an unreadable or unrecognized construct fail the run | `evidence/artifacts/notification-types-20260929/notification_types_scan.py` and `scan_reader_mutants.py`, `evidence/artifacts/login-shell-contract-20260929/landscape_refresh.py`, `evidence/artifacts/terminal-experience-20260928/color_capture.py` |
| 2026-09-30 | Naming a private string in the code that hides it | The sanitizer that replaced two private project labels in the review records listed the labels in its own source, so the repository named what it was meant to hide (an earlier record had already republished them) | Read such strings from a private file outside every checkout, refuse to run without it, and assert the finished record is free of them before it is written | `evidence/artifacts/terminal-lane-cross-family-review-20260930/record_sanitizer.py` (`private_labels`, `assert_clean`) |
6 changes: 3 additions & 3 deletions evidence/artifacts/login-shell-contract-20260929/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ the real home's placeholder names and the top level of the real `/tmp` and `/tmp

| Script | What it measures | How it was run |
| --- | --- | --- |
| `scrub_placeholder_probe.py` | What `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1` leaves behind on Linux with bubblewrap: three arms in throwaway homes (no variable; the variable with a stock Ubuntu-shaped home that has only `~/.profile`; the variable with a real `~/.bash_profile` that hands off to `~/.profile`). Runs the installed native binary under `env -i` with a dummy non-credential API value; the run stops at the API-key check after the files are written, so no model call is made. Each arm points `TMPDIR` and `CLAUDE_CODE_TMPDIR` at a short private directory (scratch is created directly under `/tmp` with a short name and an arm whose socket path would exceed the client's 103-byte cap is refused, and `XDG_RUNTIME_DIR` is never set), because the client falls back to a fixed `/tmp/cc-socks-<uid>` directory when `<dir>/cc-socks/<pid>.sock` is longer than that. In the stock-home arm it also runs the installed `claude doctor` twice, with the sandbox off and with it enabled plus a planted stale mask file (the positive control), and records whether each run's warnings name the planted file and the scrub-created file (the planted file only shows the warning is live: the documented warning covers 0-byte read-only placeholders, and the scrub-created file is writable, so its mode is recorded too). Prints counts, generic dotfile names and booleans, and whether each login `PATH` still holds `~/.local/bin`. The client also touches one absolute path outside any home, `/tmp/inline-comments-buffer.jsonl`, which it hard-codes; the script removes it only if it was absent before and appeared, empty, during the run, and reports both facts | `python3 -B scrub_placeholder_probe.py` (needs `claude` and `bwrap`) |
| `scrub_placeholder_probe.py` | What `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1` leaves behind on Linux with bubblewrap: three arms in throwaway homes (no variable; the variable with a stock Ubuntu-shaped home that has only `~/.profile`; the variable with a real `~/.bash_profile` that hands off to `~/.profile`). Runs the installed native binary under `env -i` with a dummy non-credential API value; the run stops at the API-key check after the files are written, so no model call is made. Each arm points `TMPDIR` and `CLAUDE_CODE_TMPDIR` at a short private directory (scratch is created directly under `/tmp` with a short name and an arm whose socket path would exceed the client's 103-byte cap is refused, and `XDG_RUNTIME_DIR` is never set), because the client falls back to a fixed `/tmp/cc-socks-<uid>` directory when `<dir>/cc-socks/<pid>.sock` is longer than that. In the stock-home arm it also runs the installed `claude doctor` twice, with the sandbox off and with it enabled plus a planted stale mask file (the positive control), and records whether each run's warnings name the planted file and the scrub-created file (the planted file only shows the warning is live: the documented warning covers 0-byte read-only placeholders, and the scrub-created file is writable, so its mode is recorded too). Prints counts, generic dotfile names and booleans, and whether each login `PATH` still holds `~/.local/bin`. The 19 names of the real home are compared by `lstat` before and after, from ONE final snapshot; a name whose state could not be observed (an unsearchable parent directory) is listed in `real_home_names_unmeasured` and makes `real_home_placeholder_names_unchanged` null, never true. The client also touches one absolute path outside any home, `/tmp/inline-comments-buffer.jsonl`, which it hard-codes; the script removes it only if it was absent before and appeared, empty, during the run, and reports both facts | `python3 -B scrub_placeholder_probe.py` (needs `claude` and `bwrap`) |
| `startup_placeholder_names.py` | Which paths the installed client's scrub-mode startup code creates: reads the native binary, finds the loop that opens a fixed list in append mode with parent directories created, and prints the home-relative and working-directory-relative names, the one absolute path, the open mode and whether `.bash_profile`, `.profile` and `.bash_login` are on the list. Read-only; prints names and booleans | `python3 -B startup_placeholder_names.py` (needs the native `claude`) |
| `socket_path_fallback_probe.py` | Where the client's messaging-socket directory (`cc-socks`) goes when its private temp directory is short (17 bytes) and when its socket path exceeds the client's own 103-byte cap (a 99-byte private path gives a 121-byte socket path): two runs of the same scrub start, the top-level names that appeared in the real `/tmp`, where the directory landed, whether a fallback directory already existed (then the run is not conclusive) and the shared `inline-comments-buffer.jsonl` (present before, appeared, removed). A fallback directory that appeared is removed only when it is an empty directory owned by this user. Prints counts and booleans | `python3 -B socket_path_fallback_probe.py` (needs the native `claude`) |
| `login_shell_check_controls.py` | The doctor's login-shell check (function copied from the practice repository, which has no remote) against eleven temporary homes: `~/.profile` only, an empty `~/.bash_profile` (the incident), a real hand-off `~/.bash_profile`, an empty `~/.bash_login`, no startup files, a healthy `~/.bash_profile` beside an empty `~/.bash_login` that bash never reads (must pass), a non-empty `~/.bash_profile` that exits 0 before the probe (must fail: no answer for any name), one that exits 1, a shell function named `claude` with no executable (must fail: `type -P` finds files only), a startup file that hashes `claude` to a path that does not exist with `hash -p`, and a mode-0644 `claude` on PATH (both must fail: `type -P` prints the stale or non-executable path, `exec` fails with 127 or 126, so a name counts only when the resolved path is an executable regular file). The probe prints FOUND or MISSING for every name and a final DONE; a run without them fails. The earlier check is kept in the file (`previous_check`) so the run shows it was wrong on three of the cases. With `--real-host` it also checks the running user's home, read-only | `python3 -B login_shell_check_controls.py --real-host` |
| `login_path_digest.py` | The number of entries and a SHA-256 prefix of the PATH a login shell ends up with, started the way the doctor check starts one (`env -i`, the distro default PATH, `/bin/bash -lc`). Run before and after a change to the login startup files: an unchanged digest shows the change altered nothing. The PATH is read between two nonce-marked lines, so a startup file's own output cannot pass for it and a newline inside a component stays part of the payload; a login shell that failed or ended before the record is a measurement error (exit 1, no digest); `--selftest` shows that on four temporary homes and adds startup files that log a `PATH=` line, print a plain line and print a non-UTF-8 byte, two different PATHs and a newline inside a component (ten checks) | `python3 -B login_path_digest.py [--selftest]` |
| `login_shell_check_controls.py` | The doctor's login-shell check (function copied from the practice repository, which has no remote) against eleven temporary homes: `~/.profile` only, an empty `~/.bash_profile` (the incident), a real hand-off `~/.bash_profile`, an empty `~/.bash_login`, no startup files, a healthy `~/.bash_profile` beside an empty `~/.bash_login` that bash never reads (must pass), a non-empty `~/.bash_profile` that exits 0 before the probe (must fail: no answer for any name), one that exits 1, a shell function named `claude` with no executable (must fail: `type -P` finds files only), a startup file that hashes `claude` to a path that does not exist with `hash -p`, and a mode-0644 `claude` on PATH (both must fail: `type -P` prints the stale or non-executable path, `exec` fails with 127 or 126, so a name counts only when the resolved path is an executable regular file). The probe prints FOUND or MISSING for every name and a final DONE; a run without them fails (the lines are ordinary stdout: they guard against accidental early exits, not against a startup file that replays the command string). The earlier check is kept in the file (`previous_check`) so the run shows it was wrong on three of the cases. With `--real-host` it also checks the running user's home, read-only | `python3 -B login_shell_check_controls.py --real-host` |
| `login_path_digest.py` | The number of entries and a SHA-256 prefix of the PATH a login shell ends up with, started the way the doctor check starts one (`env -i`, the distro default PATH, `/bin/bash -lc`). Run before and after a change to the login startup files: an unchanged digest shows the change altered nothing. The PATH is read between two nonce-marked lines, so a startup file's ordinary output cannot pass for it (a startup file that deliberately replays the command string (`$BASH_EXECUTION_STRING`) can, so shell stdout is not authenticated) and a newline inside a component stays part of the payload; a login shell that failed or ended before the record is a measurement error (exit 1, no digest); `--selftest` shows that on four temporary homes and adds startup files that log a `PATH=` line, print a plain line and print a non-UTF-8 byte, two different PATHs and a newline inside a component (ten checks) | `python3 -B login_path_digest.py [--selftest]` |
| `agents_json_probe.py` | What `claude agents --json` prints on this host: rows, kinds, statuses and field names, and whether `--cwd` and `--all` change it. Read-only, no session started; a live snapshot | `python3 -B agents_json_probe.py` |
| `landscape_refresh.py` | The dated upstream refresh: Windows Terminal releases and the two toast signals, the newest Claude Code and Codex releases, how many lines of Codex's notification, palette and config sources changed between the installed tag and the newest stable tag, the candidate notifiers' heads against the recorded pins, and the public Claude Code reports about the placeholder files. Reads only; never files or comments. The Codex comparison covers the Notifications, NotificationMethod and NotificationCondition enums, the TuiNotificationSettings struct, the two Display impls, Notifications' Default impl and the `terminal_title` field, each with its attributes (which may span lines) and doc comments, and answers null when a part is missing; `--selftest` needs no network | `python3 -B landscape_refresh.py [--selftest]` (needs `gh` signed in) |

Expand Down
Loading
Loading