Skip to content
claudewheel.health
Edit
On this page

Pre-launch diagnostics: symlinks, tokens, disk usage, permission drift, and deployed hook-script drift checked against the canonical guardrail model.

#claudewheel.health

#claudewheel.health

Pre-launch diagnostics: symlinks, tokens, disk usage, and permission/hook drift against the canonical guardrail model.

#HealthResult

Health check result with ok status, label, and detail message.

#check_tmpfs_quota

python
def check_tmpfs_quota() -> HealthResult

Check /tmp usage percentage via df.

#_tmp_claude_dir

python
def _tmp_claude_dir() -> Path

Return the per-user Claude scratch dir under /tmp.

#_real_disk_usage

python
def _real_disk_usage(root: Path) -> int

Sum the real tmpfs block usage of regular files under root.

Correctness requirements this satisfies:

  • Never follows symlinks. os.walk(followlinks=False) does not descend into

symlinked directories, and lstat + S_ISREG skips symlinks to files. So symlink targets living outside /tmp (Claude session dirs link into home and project dirs) are never counted -- they consume zero /tmp space.

  • Counts REAL disk usage (st_blocks * 512), not apparent st_size. tmpfs

charges by allocated blocks; apparent size overcounts sparse files.

#check_tmp_claude_size

python
def check_tmp_claude_size() -> HealthResult

Check real tmpfs usage of /tmp/claude-$UID/ (excludes symlink targets).

#_discover_profiles

python
def _discover_profiles(ws: 'Workspace') -> list[Profile]

Enumerate profiles via the workspace's ProfileStore.

Thin adapter over the shared :meth:ProfileStore.discover helper in the "swallow" mode -- the health-run carve-out: an unreadable token file must not abort a diagnostic run, so enumeration proceeds with that profile's has_token False and :func:check_tokens / :func:check_token_expiry report the unreadable entry, naming the profile. Each profile carries its own token file, so one bad file no longer decides the whole run.

#_managed_profiles

python
def _managed_profiles(ws: 'Workspace') -> list[Profile]

Discovered profiles EXCLUDING the vanilla "default" (~/.claude).

~/.claude is Claude Code's own config dir -- managed by Claude Code, not cw, and strictly read-only to cw. It is therefore EXEMPT from every guardrail/settings health check (shared symlinks, hook wiring, settings defaults, canonical/shared drift, relocated hook paths) and from the cw token check (it legitimately has no cw-managed token). Non-guardrail integrity checks (auth shadow, file permissions) still see it via :func:_discover_profiles, where a bare ~/.claude self-skips harmlessly.

python
def check_shared_symlinks(ws: 'Workspace') -> HealthResult

Verify each profile's shared dirs are symlinks to ~/.claudewheel/shared/.

#_hook_wired

python
def _hook_wired(hooks: object, event: str, matcher: str, script: str, scripts_dir: Path) -> bool

Return True if hooks wires script under event with matcher.

An entry matches when its matcher equals matcher (an absent matcher is treated as the empty string, which is how UserPromptSubmit entries are stored) and it carries a hook command equal to the EXACT canonical command for script under scripts_dir. Exact-match (not substring) so a hook pointing at a stale/dead scripts directory -- right basename, wrong root -- does NOT pass, which a substring match would have wrongly accepted.

#check_hooks_wired

python
def check_hooks_wired(ws: 'Workspace') -> HealthResult

Verify each profile wires every expected hook in settings.json.

The canonical wirings are the (event, matcher, script-name) triples in guardrail.EXPECTED_HOOK_WIRINGS. A profile passes only when every triple is present: an entry under the given event whose matcher equals the given matcher, containing a hook command equal to the exact canonical command (scripts_dir / script) for that triple. A hook pointing at the right basename under the wrong directory does NOT satisfy the wiring.

#check_settings_defaults

python
def check_settings_defaults(ws: 'Workspace') -> HealthResult

Verify each profile enforces expected defaults in settings.json.

#_diff_json

python
def _diff_json(label: str, canonical: object, actual: object) -> list[str]

Return human-readable lines describing differences between two JSON values.

#check_shared_settings_drift

python
def check_shared_settings_drift(ws: 'Workspace') -> HealthResult

Compare each profile's hooks and disallowedTools against shared-settings.json.

#_canonical_permission_diffs

python
def _canonical_permission_diffs(label: str, perms: object) -> list[str]

Return drift lines comparing a permissions block against the canonical model.

Checks permissions.deny and permissions.ask against guardrail.canonical_deny_rules() / canonical_ask_rules() (reporting missing canonical entries and extra non-canonical ones) and flags any permissions.allow entry that is a known dead/conflicting allow (guardrail.ALLOW_CONFLICTS).

#check_canonical_permissions_drift

python
def check_canonical_permissions_drift(ws: 'Workspace') -> HealthResult

Compare each profile's permissions against the canonical guardrail model.

For every profile settings.json and for shared-settings.json's profileDefaults (which seeds new profiles), verify that permissions.deny / permissions.ask match the canonical guardrail rules exactly and that no permissions.allow entry is a known dead/conflicting allow. Reports MISSING canonical entries, EXTRA non-canonical entries, and conflicting allows per profile. Warnings only -- never raises; ok is True only when everything matches and no conflicts exist.

#check_auth_shadow

python
def check_auth_shadow(ws: 'Workspace') -> HealthResult

Detect profiles where .credentials.json claudeAiOauth shadows a long-lived token.

#check_token_expiry

python
def check_token_expiry(ws: 'Workspace') -> HealthResult

Warn if any profile's token is approaching its 1-year expiry.

One read per profile: each token entry dates itself, so an unreadable entry fails the check naming that profile instead of taking the whole run's token view down with it.

#check_tokens

python
def check_tokens(ws: 'Workspace') -> HealthResult

Verify each profile holds a stored OAuth token of its own.

One read per profile, from the profile's own data directory. An unreadable entry fails the check naming that profile, with the actionable message.

#check_orphan_profiles

python
def check_orphan_profiles(ws: 'Workspace') -> HealthResult

Detect profile dirs in ~/.claudewheel/profiles/ that are not registered.

A directory is "orphan" if it: - lives in ~/.claudewheel/profiles/ - is NOT discovered by _discover_profiles() (which checks .credentials.json, settings.json, and claudewheel's own per-profile data directory) - is NOT listed in options.json's profile values

For each orphan, we also flag if it contains broken symlinks (symlinks whose target does not exist).

#check_file_permissions

python
def check_file_permissions(ws: 'Workspace') -> HealthResult

Verify sensitive files and directories have restrictive permissions.

Per profile: Claude Code's .credentials.json and claudewheel's own token entry must be 0600, and the data directory holding that entry must be 0700 -- a readable directory exposes the entry's name, size and mtime even when the file itself is locked down.

#check_inode_renames

python
def check_inode_renames(ws: 'Workspace') -> HealthResult

Detect directory renames by comparing inode records against the filesystem.

#check_deployed_hook_drift

python
def check_deployed_hook_drift(ws: 'Workspace') -> HealthResult

Compare deployed hook scripts against the generated HOOK_SCRIPTS model.

Byte-hashes each script deployed under scripts_dir against the corresponding HOOK_SCRIPTS[name] string (the canonical model, generated from the guardrail spec at import). Drift means a deployed script no longer matches what claudewheel deploy-hooks would write -- usually a stale copy left over after the model was regenerated.

Warn-only: reports drift but NEVER raises and is never a hard gate. Absence is not drift: if scripts_dir does not exist (CI, fresh machines) or an individual model script has not been deployed yet, it is skipped and the check stays OK. Only the scripts present in both HOOK_SCRIPTS and on disk are compared.

#_stale_hook_command_paths

python
def _stale_hook_command_paths(hooks: object, scripts_dir: Path) -> list[str]

Return claudewheel-managed hook commands NOT rooted at scripts_dir.

Walks every hook command under hooks and considers only commands whose basename is a known claudewheel hook script (HOOK_SCRIPTS). A managed command is "stale" when its parent directory is not scripts_dir -- i.e. it points at a scripts directory left behind by a workspace relocation. Commands for user-custom (non-claudewheel) scripts are ignored entirely, so unrelated hooks under any directory are preserved.

#check_relocated_hook_paths

python
def check_relocated_hook_paths(ws: 'Workspace') -> HealthResult

Detect hook commands pointing at a scripts dir other than the current one.

The deployed-hook drift check compares script CONTENT hashes and so cannot see a hook whose command still references a STALE absolute scripts directory after the workspace was relocated (the substring matcher in check_hooks_wired also passes for a stale root). This check closes that blind spot: for shared-settings.json and every profile's settings.json, it flags any claudewheel-managed hook command whose parent directory is not the current scripts_dir. Intact (current-root) and absent hooks pass; claudewheel patch-profiles repaths any it finds.

#run_health_check

python
def run_health_check(ws: 'Workspace') -> list[HealthResult]

Run all health checks and return results.

Token data is read per profile, inside the checks that need it: an unreadable entry fails check_tokens / check_token_expiry naming the profile, while every other check still runs (enumeration tolerates it and reports that profile as tokenless).

python
def print_health_report(results: list[HealthResult], file: IO[str] | None=None) -> None

Print health check results. Defaults to stdout; pass file=sys.stderr for non-interactive mode.

More tools from this site

  • claudestream Drive Claude Code from Python: run it as a subprocess and read its output as typed events, with async and sync sessions, sandbox policies, and tools you define in Python
  • dirstat Fast, single-binary directory statistics CLI: every file under a tree grouped by format, with counts, sizes, and lines of code, as a colored terminal table or as JSON
  • fastware A batteries-included ASGI framework: msgspec JSON, a managed Granian server, dependency injection, SSE, WebSockets, auth, and a test client
  • go-toml-edit Zero-dep TOML editing library for Go with comment preservation
  • howmuchleft The fastest Claude Code statusline: context window, 5-hour, and weekly limit usage as three customizable gradient bars, rendering in about 6 ms
  • orxtra
  • pgdesign
  • predraw Declarative rendering pipeline: describe a scene in JSON and get SVG, PNG and WebP out, with light and dark style tokens, reusable components and text converted to path outlines
  • reposummary Turn a git repository's history into a Markdown journal: pick a time window or revision range and get a readable digest of what changed, optionally narrated by an LLM
  • rlsbl Release orchestration and project scaffolding CLI that bumps versions, validates a structured JSONL changelog, tags only the commit CI verified, and publishes to npm, PyPI, Go and more
  • safegit git wrapper CLI that gives each commit its own temporary index and retries ref updates on conflict, so concurrent agents share one repository
  • saferm Command-line replacement for rm that archives every deletion with a mandatory reason and the context it ran in, so deleted files can be listed, inspected and restored
  • selfdoc Static Site Generator that builds a project's documentation site directly from its source code, so the docs can never drift from the code they describe, with SEO/AEO, first-class blog, search, and cross-project linking built in
  • strictcli
  • stricttest An always-on test-isolation floor: a pytest plugin and a Go env-hygiene module that make a test suite structurally unable to reach real credentials, the real HOME, the network, or the development repository.
  • wesktop A Python framework that turns an ASGI web app into a desktop application, serving it from a local Granian server and displaying it in a native OS window via pywebview
Search