#!/usr/bin/env python3 """`/verbose`, TUI-side: flip Claude Code's *persisted* verbose view for the current project. Run by hooks/local-commands.py from the UserPromptExpansion hook, i.e. when you type `/verbose [on|off|status]` -- the hook executes this and blocks the skill expansion, so no prompt ever reaches the model and the output below is shown to you directly. (On a host without that hook, skills/verbose/SKILL.md falls back to having the model run it.) Claude Code has no built-in runtime `/verbose`; the live toggle is a keypress (`ctrl+x v` here, `ctrl+o` by default -- `app:toggleTranscript`). What can be scripted is the persisted default: `viewMode` ("verbose" | "default" | "focus" -- this is what actually decides the view; it also beats a sticky `/focus` choice for new sessions), plus `verbose` (full tool input/output inline) and `showThinkingSummaries` for versions without viewMode. All accept any settings scope, so this writes the project's `.claude/settings.local.json` -- local to this machine, never committed -- instead of the dots-tracked ~/.claude/settings.json, which would show a diff on every toggle. Project-local settings win over user settings, so the user default stays `verbose: true` and a project can be quietened individually. usage: local.py [on|off|toggle|status] (no argument = toggle) """ import json import os import subprocess import sys KEYS = ("verbose", "showThinkingSummaries") VIEW = "viewMode" # "verbose" | "default" | "focus"; overrides KEYS and the sticky /focus choice LIVE = "live view: ctrl+x v (or ctrl+o) toggles the transcript; ctrl+e inside it shows everything" def project_root(): try: r = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True, timeout=2) if r.returncode == 0 and r.stdout.strip(): return r.stdout.strip() except Exception: pass return os.getcwd() def load(path): try: with open(path) as fh: return json.load(fh) except FileNotFoundError: return {} except Exception as e: # a broken local file must not be silently overwritten sys.exit(f"error: cannot parse {path}: {e}") def main(): raw = " ".join(sys.argv[1:]).strip() mode = raw.lower() or "toggle" if mode not in ("on", "off", "toggle", "status"): sys.exit("usage: /verbose [on|off|status]") root = project_root() local_path = os.path.join(root, ".claude", "settings.local.json") user = load(os.path.expanduser("~/.claude/settings.json")) local = load(local_path) effective = {k: bool(local.get(k, user.get(k, False))) for k in KEYS} view = local.get(VIEW, user.get(VIEW)) def state_of(view, eff): if view == "verbose": return "ON" if view in ("default", "focus"): return f"OFF ({view} forced by settings)" if all(eff.values()): return "ON via the verbose key (viewMode unset: a sticky /focus choice still wins)" return "OFF" if not any(eff.values()) else "MIXED" if mode != "status": current_on = state_of(view, effective).startswith("ON") on = {"on": True, "off": False}.get(mode, not current_on) if on: local[VIEW] = "verbose" # forces verbose for new sessions, beating a sticky /focus else: local.pop(VIEW, None) # release: the sticky /focus choice (or default) applies again for k in KEYS: local[k] = on os.makedirs(os.path.dirname(local_path), exist_ok=True) with open(local_path, "w") as fh: json.dump(local, fh, indent=2) fh.write("\n") effective = {k: on for k in KEYS} view = local.get(VIEW, user.get(VIEW)) print(f"verbose view: {'ON' if on else 'OFF'} for {root}") print(f" written to {local_path} (viewMode applies to new sessions; the current one: ctrl+x v)") else: print(f"verbose view: {state_of(view, effective)} for {root}") print(f" {VIEW}: {view or '-'} (project-local: {local.get(VIEW, '-')}, user: {user.get(VIEW, '-')})") for k in KEYS: print(f" {k}: {str(effective[k]).lower()} (project-local: {local.get(k, '-')}, user: {user.get(k, '-')})") print(LIVE) if __name__ == "__main__": main()