#!/usr/bin/env python3 """session_start.py — aggregated deterministic status pass for the cc-os plugin family. Runs every registered check in-process (checks.REGISTRY) and emits at most one warning banner plus any near-zero-token notes. ok -> silent note -> additionalContext line (never snoozed or suppressed) warn -> one line in the single systemMessage banner; snoozed once per day and permanently suppressible via .cc-os/suppress- Outside a git project only environment-scoped checks run and no state is written. Always exits 0 — the hook must never block a session. """ from __future__ import annotations import json import os import signal import sys from datetime import date from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from checks import DEFAULT_VAULT_PATH, REGISTRY, CheckResult, Ctx # noqa: E402 from state import StateDir, find_project_root, read_config # noqa: E402 CHECK_BUDGET_SECONDS = 2 BANNER_HEADER = ( "[os-status] Session preconditions need attention (run /os-status:fix to" " address all of these):" ) def run_isolated(check, ctx) -> CheckResult: """Failure isolation: an exception or a blown time budget becomes a generic warn naming the check; the remaining checks keep running.""" def timed_out(signum, frame): raise TimeoutError alarmed = hasattr(signal, "SIGALRM") if alarmed: previous = signal.signal(signal.SIGALRM, timed_out) signal.alarm(CHECK_BUDGET_SECONDS) try: result = check.fn(ctx) if not isinstance(result, CheckResult): raise TypeError("check returned a non-CheckResult") return result except Exception: return CheckResult("warn", f"status check '{check.name}' failed to run") finally: if alarmed: signal.alarm(0) signal.signal(signal.SIGALRM, previous) def run(ctx: Ctx, state, today: date, out) -> None: notes, warns = [], [] for check in REGISTRY: if check.project_scoped and ctx.project_root is None: continue result = run_isolated(check, ctx) if result.status == "note": notes.append(result.message) elif result.status == "warn": if state and ( state.suppressed(check.name) or state.snoozed(check.name, today) ): continue warns.append((check.name, result.message)) payload = {} if warns: lines = [BANNER_HEADER] lines.extend(f" - {message}" for _, message in warns) if state: names = ", ".join(name for name, _ in warns) lines.append( f" (silence one permanently: touch .cc-os/suppress-; warned: {names})" ) for name, _ in warns: state.stamp(name, today) payload["systemMessage"] = "\n".join(lines) if notes: payload["hookSpecificOutput"] = { "hookEventName": "SessionStart", "additionalContext": "\n".join(notes), } if payload: out.write(json.dumps(payload)) def main() -> int: try: root = find_project_root(Path.cwd()) config = read_config(root) if root else {} ctx = Ctx( project_root=root, settings_path=Path.home() / ".claude" / "settings.json", vault_path=Path( os.path.expanduser(config.get("vault_path", DEFAULT_VAULT_PATH)) ), config=config, environ=dict(os.environ), ) state = StateDir(root) if root else None run(ctx, state, date.today(), sys.stdout) except Exception: pass # never block a session return 0 if __name__ == "__main__": sys.exit(main())