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
5 changes: 5 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ jobs:
- name: install.py transactional self-test (no host/state dir touched)
run: python3 installers/install.py --self-test

- name: Opt-in SessionStart update-notify hook test (settings merge + hook logic, offline)
run: python3 installers/tests/test_session_hook.py

- name: Release-ZIP provenance + updater-consumability round-trip (build -> safe-extract)
run: bash installers/tests/test_release_zip.sh

Expand Down Expand Up @@ -246,5 +249,7 @@ jobs:
run: python installers/tests/test_txn.py
- name: Updater verify / safe-extract / check-update test (offline)
run: python installers/tests/test_update.py
- name: Opt-in SessionStart update-notify hook test (settings merge + hook logic)
run: python installers/tests/test_session_hook.py
- name: install.py transactional self-test
run: python installers/install.py --self-test
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@

### Added

- **Opt-in update notice for Claude Code (off by default).** `install.py --enable-update-notify`
merges a SessionStart hook (`installers/session_update_check.py`) into `~/.claude/settings.json`
that prints a one-line "update available" `systemMessage` at session start; `--disable-update-notify`
removes only that hook. The hook **does not read the SessionStart stdin** (no cwd/transcript/session
id), has no telemetry/analytics/unique-id, uses the shared clock-sane 24h cache + a short timeout,
stays silent on any error (never blocks a session), honors `MEDSCI_NO_UPDATE_CHECK=1`, and installs
nothing — it only notifies. A version *check* now resolves the latest tag without requiring the
OS-specific download asset (`resolve_latest_tag`), so the notice works on Linux too. Settings merge
is idempotent, preserves foreign hooks/settings, removes only ours (incl. mixed entries), and
refuses to clobber an unparseable `settings.json`. Tested offline (`installers/tests/test_session_hook.py`,
26 cases) on Ubuntu + macOS + Windows.
- **Release-pipeline supply-chain hardening (self-update foundation, no user-facing change).**
`release.yml` now: gates on a version-consistency check (the pushed tag must equal
`CITATION.cff` == `package.json` == `metadata/distribution_manifest.json`); injects a verified
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,10 @@ MedSci Skills updates often. You do **not** need GitHub, git, or the command lin
- **Terminal users:** `npx medsci-skills@latest install` always installs the latest.
- **Just checking:** `python3 installers/install.py --check-update` reports whether a newer version
is available and installs nothing.
- **Get reminded (opt-in, Claude Code):** `python3 installers/install.py --enable-update-notify`
shows a one-line *"update available"* notice when a Claude Code session starts. It is **off by
default**, checks at most once a day, reads nothing about your session, and never installs
anything. Turn it off with `--disable-update-notify`, or silence it with `MEDSCI_NO_UPDATE_CHECK=1`.
- **Claude Code plugin marketplace:** third-party marketplace **auto-update is off by default** —
enable it in Claude Code or run a manual plugin update.

Expand Down
23 changes: 21 additions & 2 deletions docs/update_privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,31 @@ Everything the updater writes stays on your computer under `~/.medsci-skills/`:

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

## Opt-in session-start notice (Claude Code)

`python3 installers/install.py --enable-update-notify` registers a Claude Code **SessionStart** hook
that prints a one-line *"update available"* notice and nothing else. It is **off by default** and is
the only thing that writes to `~/.claude/settings.json` (it merges one hook entry and preserves your
existing settings).

The hook is built to be safe:

- It **does not read the SessionStart input** — your working directory, transcript path, and session
id are never read or transmitted. There is no telemetry, no analytics, and no unique id.
- Its only network call is the same single GitHub version GET described above, made **at most once a
day** (a 24-hour cache), with a short timeout. On any error or timeout it stays silent, so it never
delays or blocks a session.
- It honors `MEDSCI_NO_UPDATE_CHECK=1` and installs nothing — it only *tells* you an update exists.

Remove it any time with `python3 installers/install.py --disable-update-notify` (which removes only
that hook and leaves the rest of `settings.json` intact).

## Checking, opting out, and uninstalling

- **Check only:** `python3 installers/install.py --check-update` reports whether a newer version
exists and installs nothing.
- **Skip the per-session update check** (if you opted into it): set the environment variable
`MEDSCI_NO_UPDATE_CHECK=1`.
- **Turn off the opt-in session-start notice:** `python3 installers/install.py --disable-update-notify`,
or set `MEDSCI_NO_UPDATE_CHECK=1` to silence it without removing it.
- **Uninstall the updater / state:** delete the `~/.medsci-skills/` folder (and any Desktop
"Update MedSci Skills" launcher you chose to create). The installed skills under
`~/.claude/skills` / `~/.agents/skills` are separate and remain until you remove them.
Expand Down
30 changes: 30 additions & 0 deletions installers/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,17 @@ def parse_args() -> argparse.Namespace:
action="store_true",
help="With your consent, also place an 'Update MedSci Skills' launcher on your Desktop.",
)
parser.add_argument(
"--enable-update-notify",
action="store_true",
help="Opt in: show a one-line 'update available' notice at Claude Code session start "
"(merges a hook into ~/.claude/settings.json; 24h-cached; no telemetry).",
)
parser.add_argument(
"--disable-update-notify",
action="store_true",
help="Opt out: remove the session-start update-notice hook from ~/.claude/settings.json.",
)
return parser.parse_args()


Expand All @@ -218,6 +229,25 @@ def main() -> int:
except Exception as exc: # noqa: BLE001
print(f"MedSci Skills: update check unavailable ({exc}).", file=sys.stderr)
return 1
if args.enable_update_notify or args.disable_update_notify:
try:
import update # noqa: PLC0415
home = medsci_txn.state_home()
if args.disable_update_notify:
r = update.unregister_session_hook(home, update.default_settings_path())
print("Session-start update notice disabled." if r == "disabled"
else "Session-start update notice was not enabled; nothing to do.")
return 0
# Opt-in: ensure the updater home (with the hook script) exists, then register the hook.
update.install_updater_home(REPO_ROOT, home, lambda _m: None)
r = update.register_session_hook(home, update.default_settings_path())
print("Opted in: Claude Code will show a one-line update notice at session start "
"(24h-cached, no telemetry). Disable with: install.py --disable-update-notify"
if r == "enabled" else "Already opted in to the session-start update notice; no change.")
return 0
except Exception as exc: # noqa: BLE001
print(f"MedSci Skills: could not change the update-notify setting ({exc}).", file=sys.stderr)
return 1
log_lines: list[str] = []
log("MedSci Skills Installer", log_lines)
log(f"Repository: {REPO_ROOT}", log_lines)
Expand Down
71 changes: 71 additions & 0 deletions installers/session_update_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Opt-in Claude Code SessionStart notifier for MedSci Skills.

Prints a single `{"systemMessage": ...}` line when a newer release exists, or nothing. It is OFF by
default — registered into ~/.claude/settings.json only on explicit opt-in
(`install.py --enable-update-notify`) and removed by `install.py --disable-update-notify`.

Privacy & safety, by construction:
* Does NOT read the SessionStart stdin — your cwd, transcript path, and session id are never read
or transmitted. No telemetry, no analytics, no unique install id.
* The only network call is a single GitHub version GET, and only when the shared 24h cache is
stale. It uses a short timeout and exits SILENTLY on any error/timeout, so it never delays or
blocks a session.
* Honors `MEDSCI_NO_UPDATE_CHECK=1` (silent, no network).
* Surfaces via `systemMessage` (shown to you), not stdout context — it adds nothing to the model's
context.

Lives next to update.py / medsci_txn.py in ~/.medsci-skills/updater/ and reuses their cached,
clock-sane version check.
"""

from __future__ import annotations

import json
import os
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import medsci_txn # noqa: E402
import update # noqa: E402

HOOK_HTTP_TIMEOUT = 4 # keep SessionStart snappy; only the rare cache-miss path waits at all


def main() -> int:
# Never read stdin (privacy). Never raise. Worst case: print nothing.
if os.environ.get("MEDSCI_NO_UPDATE_CHECK"):
return 0
try:
home = medsci_txn.state_home()
inst = update.installed_version(home)
inst_v = update.parse_semver(inst)
if inst_v is None:
return 0 # not installed via the transactional installer -> say nothing

tag = update._cache_fresh(home) # clock-sane 24h cache (shared with --check-update)
if tag is None:
try:
tag = update.resolve_latest_tag(
lambda u: update._real_get_json(u, timeout=HOOK_HTTP_TIMEOUT)
)
update._cache_store(home, tag)
except Exception: # noqa: BLE001 - offline / slow / API error -> stay silent, never block
return 0

latest_v = update.parse_semver(tag[1:] if tag.startswith("v") else tag)
if latest_v and latest_v > inst_v:
msg = (
f"MedSci Skills update available — installed {inst}, latest {tag}. "
f"Run the updater in ~/.medsci-skills/updater/ (or the 'Update MedSci Skills' Desktop "
f"launcher) to update. Set MEDSCI_NO_UPDATE_CHECK=1 to silence this notice."
)
print(json.dumps({"systemMessage": msg, "suppressOutput": True}))
except Exception: # noqa: BLE001 - a notifier must never disrupt a session
return 0
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading