Skip to content

Commit 4ea3f94

Browse files
authored
Merge pull request #180 from Aperivue/feat/v46-update-notify-hook
feat(updater): opt-in SessionStart update notice for Claude Code (off by default) (PR-3)
2 parents d14e852 + 3ce055f commit 4ea3f94

10 files changed

Lines changed: 639 additions & 9 deletions

File tree

.github/workflows/validate.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ jobs:
4949
- name: install.py transactional self-test (no host/state dir touched)
5050
run: python3 installers/install.py --self-test
5151

52+
- name: Opt-in SessionStart update-notify hook test (settings merge + hook logic, offline)
53+
run: python3 installers/tests/test_session_hook.py
54+
5255
- name: Release-ZIP provenance + updater-consumability round-trip (build -> safe-extract)
5356
run: bash installers/tests/test_release_zip.sh
5457

@@ -246,5 +249,7 @@ jobs:
246249
run: python installers/tests/test_txn.py
247250
- name: Updater verify / safe-extract / check-update test (offline)
248251
run: python installers/tests/test_update.py
252+
- name: Opt-in SessionStart update-notify hook test (settings merge + hook logic)
253+
run: python installers/tests/test_session_hook.py
249254
- name: install.py transactional self-test
250255
run: python installers/install.py --self-test

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@
44

55
### Added
66

7+
- **Opt-in update notice for Claude Code (off by default).** `install.py --enable-update-notify`
8+
merges a SessionStart hook (`installers/session_update_check.py`) into `~/.claude/settings.json`
9+
that prints a one-line "update available" `systemMessage` at session start; `--disable-update-notify`
10+
removes only that hook. The hook **does not read the SessionStart stdin** (no cwd/transcript/session
11+
id), has no telemetry/analytics/unique-id, uses the shared clock-sane 24h cache + a short timeout,
12+
stays silent on any error (never blocks a session), honors `MEDSCI_NO_UPDATE_CHECK=1`, and installs
13+
nothing — it only notifies. A version *check* now resolves the latest tag without requiring the
14+
OS-specific download asset (`resolve_latest_tag`), so the notice works on Linux too. Settings merge
15+
is idempotent, preserves foreign hooks/settings, removes only ours (incl. mixed entries), and
16+
refuses to clobber an unparseable `settings.json`. Tested offline (`installers/tests/test_session_hook.py`,
17+
26 cases) on Ubuntu + macOS + Windows.
718
- **Release-pipeline supply-chain hardening (self-update foundation, no user-facing change).**
819
`release.yml` now: gates on a version-consistency check (the pushed tag must equal
920
`CITATION.cff` == `package.json` == `metadata/distribution_manifest.json`); injects a verified

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,10 @@ MedSci Skills updates often. You do **not** need GitHub, git, or the command lin
539539
- **Terminal users:** `npx medsci-skills@latest install` always installs the latest.
540540
- **Just checking:** `python3 installers/install.py --check-update` reports whether a newer version
541541
is available and installs nothing.
542+
- **Get reminded (opt-in, Claude Code):** `python3 installers/install.py --enable-update-notify`
543+
shows a one-line *"update available"* notice when a Claude Code session starts. It is **off by
544+
default**, checks at most once a day, reads nothing about your session, and never installs
545+
anything. Turn it off with `--disable-update-notify`, or silence it with `MEDSCI_NO_UPDATE_CHECK=1`.
542546
- **Claude Code plugin marketplace:** third-party marketplace **auto-update is off by default**
543547
enable it in Claude Code or run a manual plugin update.
544548

docs/update_privacy.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,31 @@ Everything the updater writes stays on your computer under `~/.medsci-skills/`:
3434

3535
Install logs are written next to the installer and **mask your home directory as `~`**.
3636

37+
## Opt-in session-start notice (Claude Code)
38+
39+
`python3 installers/install.py --enable-update-notify` registers a Claude Code **SessionStart** hook
40+
that prints a one-line *"update available"* notice and nothing else. It is **off by default** and is
41+
the only thing that writes to `~/.claude/settings.json` (it merges one hook entry and preserves your
42+
existing settings).
43+
44+
The hook is built to be safe:
45+
46+
- It **does not read the SessionStart input** — your working directory, transcript path, and session
47+
id are never read or transmitted. There is no telemetry, no analytics, and no unique id.
48+
- Its only network call is the same single GitHub version GET described above, made **at most once a
49+
day** (a 24-hour cache), with a short timeout. On any error or timeout it stays silent, so it never
50+
delays or blocks a session.
51+
- It honors `MEDSCI_NO_UPDATE_CHECK=1` and installs nothing — it only *tells* you an update exists.
52+
53+
Remove it any time with `python3 installers/install.py --disable-update-notify` (which removes only
54+
that hook and leaves the rest of `settings.json` intact).
55+
3756
## Checking, opting out, and uninstalling
3857

3958
- **Check only:** `python3 installers/install.py --check-update` reports whether a newer version
4059
exists and installs nothing.
41-
- **Skip the per-session update check** (if you opted into it): set the environment variable
42-
`MEDSCI_NO_UPDATE_CHECK=1`.
60+
- **Turn off the opt-in session-start notice:** `python3 installers/install.py --disable-update-notify`,
61+
or set `MEDSCI_NO_UPDATE_CHECK=1` to silence it without removing it.
4362
- **Uninstall the updater / state:** delete the `~/.medsci-skills/` folder (and any Desktop
4463
"Update MedSci Skills" launcher you chose to create). The installed skills under
4564
`~/.claude/skills` / `~/.agents/skills` are separate and remain until you remove them.

installers/install.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,17 @@ def parse_args() -> argparse.Namespace:
204204
action="store_true",
205205
help="With your consent, also place an 'Update MedSci Skills' launcher on your Desktop.",
206206
)
207+
parser.add_argument(
208+
"--enable-update-notify",
209+
action="store_true",
210+
help="Opt in: show a one-line 'update available' notice at Claude Code session start "
211+
"(merges a hook into ~/.claude/settings.json; 24h-cached; no telemetry).",
212+
)
213+
parser.add_argument(
214+
"--disable-update-notify",
215+
action="store_true",
216+
help="Opt out: remove the session-start update-notice hook from ~/.claude/settings.json.",
217+
)
207218
return parser.parse_args()
208219

209220

@@ -218,6 +229,25 @@ def main() -> int:
218229
except Exception as exc: # noqa: BLE001
219230
print(f"MedSci Skills: update check unavailable ({exc}).", file=sys.stderr)
220231
return 1
232+
if args.enable_update_notify or args.disable_update_notify:
233+
try:
234+
import update # noqa: PLC0415
235+
home = medsci_txn.state_home()
236+
if args.disable_update_notify:
237+
r = update.unregister_session_hook(home, update.default_settings_path())
238+
print("Session-start update notice disabled." if r == "disabled"
239+
else "Session-start update notice was not enabled; nothing to do.")
240+
return 0
241+
# Opt-in: ensure the updater home (with the hook script) exists, then register the hook.
242+
update.install_updater_home(REPO_ROOT, home, lambda _m: None)
243+
r = update.register_session_hook(home, update.default_settings_path())
244+
print("Opted in: Claude Code will show a one-line update notice at session start "
245+
"(24h-cached, no telemetry). Disable with: install.py --disable-update-notify"
246+
if r == "enabled" else "Already opted in to the session-start update notice; no change.")
247+
return 0
248+
except Exception as exc: # noqa: BLE001
249+
print(f"MedSci Skills: could not change the update-notify setting ({exc}).", file=sys.stderr)
250+
return 1
221251
log_lines: list[str] = []
222252
log("MedSci Skills Installer", log_lines)
223253
log(f"Repository: {REPO_ROOT}", log_lines)

installers/session_update_check.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env python3
2+
"""Opt-in Claude Code SessionStart notifier for MedSci Skills.
3+
4+
Prints a single `{"systemMessage": ...}` line when a newer release exists, or nothing. It is OFF by
5+
default — registered into ~/.claude/settings.json only on explicit opt-in
6+
(`install.py --enable-update-notify`) and removed by `install.py --disable-update-notify`.
7+
8+
Privacy & safety, by construction:
9+
* Does NOT read the SessionStart stdin — your cwd, transcript path, and session id are never read
10+
or transmitted. No telemetry, no analytics, no unique install id.
11+
* The only network call is a single GitHub version GET, and only when the shared 24h cache is
12+
stale. It uses a short timeout and exits SILENTLY on any error/timeout, so it never delays or
13+
blocks a session.
14+
* Honors `MEDSCI_NO_UPDATE_CHECK=1` (silent, no network).
15+
* Surfaces via `systemMessage` (shown to you), not stdout context — it adds nothing to the model's
16+
context.
17+
18+
Lives next to update.py / medsci_txn.py in ~/.medsci-skills/updater/ and reuses their cached,
19+
clock-sane version check.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
import json
25+
import os
26+
import sys
27+
from pathlib import Path
28+
29+
sys.path.insert(0, str(Path(__file__).resolve().parent))
30+
import medsci_txn # noqa: E402
31+
import update # noqa: E402
32+
33+
HOOK_HTTP_TIMEOUT = 4 # keep SessionStart snappy; only the rare cache-miss path waits at all
34+
35+
36+
def main() -> int:
37+
# Never read stdin (privacy). Never raise. Worst case: print nothing.
38+
if os.environ.get("MEDSCI_NO_UPDATE_CHECK"):
39+
return 0
40+
try:
41+
home = medsci_txn.state_home()
42+
inst = update.installed_version(home)
43+
inst_v = update.parse_semver(inst)
44+
if inst_v is None:
45+
return 0 # not installed via the transactional installer -> say nothing
46+
47+
tag = update._cache_fresh(home) # clock-sane 24h cache (shared with --check-update)
48+
if tag is None:
49+
try:
50+
tag = update.resolve_latest_tag(
51+
lambda u: update._real_get_json(u, timeout=HOOK_HTTP_TIMEOUT)
52+
)
53+
update._cache_store(home, tag)
54+
except Exception: # noqa: BLE001 - offline / slow / API error -> stay silent, never block
55+
return 0
56+
57+
latest_v = update.parse_semver(tag[1:] if tag.startswith("v") else tag)
58+
if latest_v and latest_v > inst_v:
59+
msg = (
60+
f"MedSci Skills update available — installed {inst}, latest {tag}. "
61+
f"Run the updater in ~/.medsci-skills/updater/ (or the 'Update MedSci Skills' Desktop "
62+
f"launcher) to update. Set MEDSCI_NO_UPDATE_CHECK=1 to silence this notice."
63+
)
64+
print(json.dumps({"systemMessage": msg, "suppressOutput": True}))
65+
except Exception: # noqa: BLE001 - a notifier must never disrupt a session
66+
return 0
67+
return 0
68+
69+
70+
if __name__ == "__main__":
71+
raise SystemExit(main())

0 commit comments

Comments
 (0)