Advisor Mode

workflow ~2149 tokens updated 2026-10-12

Session posture for expensive orchestrator models (Fable/Opus): /advisor puts the main agent into a delegation-only mode - it writes specs, dispatches cheaper implementer agents, personally reviews spec compliance and quality by default (cross-provider review is opt-in), triages findings, and merges, while a hard PreToolUse gate (exit 2, bypass-surviving) blocks its own file edits and non-orchestration Bash. Subagent calls pass untouched; a per-turn posture injection keeps the model delegating instead of fighting denials. ADVISOR_DIRECT=1 one-off hatch. State is per session: every session starts in the mode, and CCGM_ADVISOR_AUTO=false opts out.

Tags

  • orchestration
  • delegation
  • hooks
  • enforcement
  • multi-agent
  • cost

README

Advisor Mode

Session posture for expensive orchestrator models (Fable/Opus): the main agent specs, delegates, reviews, and merges — it never implements. Enforcement is mechanical: while the mode is on, a PreToolUse hook hard-blocks the main agent's file edits and non-orchestration Bash (exit 2, which survives bypass mode). Subagent tool calls pass untouched, so delegated implementers work normally.

/etp already runs the delegate → personal lead review → fix → follow-ups loop for ready work (plans, investigated issues). Advisor mode is the standing posture that routes ad-hoc work through the same loop instead of letting the expensive model implement inline — and hands etp-shaped work to /etp.

Why hard enforcement

Prompt-only "you never implement" orchestrators demonstrably drift into hands-on patching under integration pressure; every documented production fix for that drift was capability removal, not better prompting. Full prior-art research: ~/code/docs/research/claude-code-advisor-delegation-mode/research.md. The guard's main/subagent discriminator (hook-input agent_id/agent_type, absent on main-agent calls) and the per-turn posture injection are adapted from the baton plugin's design. Discriminator drift is asymmetric: main-agent inputs gaining the fields makes the guard inert (fails open, visibly); subagent inputs losing them would deny subagents too — loud and recoverable with /advisor off, never a silent misroute.

State is per session

The flag is ~/.claude/advisor-mode/<session_id>, keyed by the session_id every hook input carries (CLAUDE_CODE_SESSION_ID is the fallback). One session's mode never binds another's — with many sessions running at once, a single machine-global file meant one session's /advisor off removed the gate from all of them.

Every session starts in advisor mode. Opt out with CCGM_ADVISOR_AUTO=false in the environment or in ~/.claude/.ccgm.env. Only compaction is exempt from the auto-on, so /advisor off survives a compaction but a resume or a /clear starts the mode again. The flag is removed when the session ends, and flags left by sessions that died are swept at the next session start.

Files

File Role
commands/advisor.md /advisor toggles this session; explicit on|off|status accepted — status also lists every session in the mode
rules/advisor-mode.md The posture contract: loop, delegation ladder, floor, review contract
hooks/advisor-guard.py PreToolUse exit-2 gate: file writes confined to work-product paths; Bash default-deny with read-only + orchestration allowlist
hooks/advisor-posture.py UserPromptSubmit injection while this session's flag exists; also names the session id
hooks/advisor-session-start.py SessionStart: creates this session's flag, migrates the legacy global file, sweeps dead sessions' flags
hooks/advisor-session-end.py SessionEnd: removes this session's flag
settings.partial.json Hook registration (merged into settings.json)

What the main agent keeps

Reading, conversation, spec-writing, agent dispatch, findings triage, and the orchestration verbs: read-only git, git checkout/switch/pull/fetch/worktree, gh PR/issue/run/label management including gh pr merge, and file writes under ~/.claude/, temp/scratchpad roots, ~/code/plans/, ~/code/docs/, worktree checkouts, and plan-mode plan files.

Read-only recon passes too: dev-tool version and identity probes (node -v, wrangler whoami, not pnpm install), shell grouping tokens, and $(...)/backtick substitution whose inner commands are themselves allowlisted (checked recursively, depth-capped, backtick bodies unescaped first). What a checked substitution returns is an argument for read-only commands only: echo $(git rev-parse HEAD) passes, sed $(echo -i) … does not.

Every argument is read the way bash will pass it — quotes removed ('-i', -''i, $'-i', $"-i"), adjacent spans joined, backslash escapes and line continuations dropped — so spelling a flag differently no longer hides it. What bash does that the guard cannot reproduce is refused instead of guessed at, because brace expansion, $IFS word splitting and globbing each turn one typed word into several: a brace group (-delet{e,e}), an unquoted glob (-dele*, [-]delete), an unquoted expansion the guard cannot resolve anywhere in a word (find <dir>$IFS-delete), a resolvable one whose value carries whitespace or a glob character (bash globs a value after it substitutes it), and an undecodable $'…' escape are all denied for a command that is not read-only. A double-quoted expansion cannot be word-split, so it keeps the narrower rule — denied leading or inside a flag before its first =, allowed after it, which is what keeps gh pr edit 1 --title="$T" usable. echo $A, grep $PAT f and rm -rf $TMPDIR/x still pass. A relative path after a cd is denied, and so are ~+, ~- and ~N anywhere: the guard resolves them against its own working directory, not the one bash will be in. The guard resolves $CLAUDE_CODE_SESSION_ID from its own validated session context, so the per-session /advisor on/off flag commands keep working even when the hook subprocess does not carry the variable.

Escape hatches

  • /advisor off — end the mode
  • ADVISOR_DIRECT=1 (env or inline) — one-off, for user-directed exceptions

Manual install

cp commands/advisor.md ~/.claude/commands/
cp rules/advisor-mode.md ~/.claude/rules/
cp hooks/advisor-guard.py hooks/advisor-posture.py ~/.claude/hooks/
cp hooks/advisor-session-start.py hooks/advisor-session-end.py ~/.claude/hooks/
# merge settings.partial.json into ~/.claude/settings.json

Cross-agent pilot compatibility

X-Plan, adrev and ETP use personal lead review by default. Explicit --cross-provider or a clear natural-language request enables the installed cross_agent_review_policy.py orchestration shim. Its early preflight checks both binaries/login status; default review needs neither. The guard permits only enumerated policy operations, with private runs under ~/.claude/cross-agent-review/<run-id>/; it does not permit direct transport scripts, arbitrary Python, reviewed-source writes or execution of recorded check argv. Trusted policy code and its resolved source cannot be overwritten through a work-product symlink.

Main-agent execution requires the installed shim and its local imports to resolve outside advisor write roots, typically through canonical repository symlinks; copied installations require delegation. A marketplace-only Claude install lacks the trusted shim. Use canonical CCGM installation in symlink mode or delegate setup/execution under normal permissions; do not broaden the guard. Optional provider errors stop that run and preserve reports/findings. The lead may separately judge delivery using evidence and normal checks, without calling the stopped run approved. Coordinator repairs require no recursive provider consensus. The optional native run has a hard two-correction-cycle cap, three exchanges per stage and no extensions. Ordinary lead review retains its separate three-round checkpoint; further lead work needs new evidence and a viable next check. Check records and host receipts are attestations, not independently proven execution/reception.

Tests

bash modules/advisor-mode/tests/test-advisor-guard.sh
bash modules/advisor-mode/tests/test-advisor-guard-differential.sh
bash modules/advisor-mode/tests/test-advisor-session.sh

test-advisor-guard.sh covers flag on/off, subagent passthrough, hatch, work-product path allowances, the Bash allowlist/denylist, redirection scoping, tool probes, grouping tokens, recursive substitution checking, quote and escape handling, malformed input, and regression probes for real bypasses found during development (newline-hidden commands, single-& chaining, sed -i variants, git checkout -- pathspec, nested escaped backticks, a substitution standing in for a flag or path, an escaped quote hiding a trailing command inside a double-quoted or $'…' span, a variable standing in for a flag or path, and a flag spelled out of reach of the literal scans by quoting, ANSI-C quoting, or an expansion).

test-advisor-guard-differential.sh compares the guard against real bash. It builds a throwaway sandbox and runs a generated carrier x command matrix — every way of spelling a mutating flag against every command that acts on one — through both the guard and bash -c, then asserts the one invariant that matters: if bash mutated the sandbox, the guard exited 2. This is the method that found issue #1017.

test-advisor-session.sh covers the per-session state: two sessions with opposite modes, the session-id fallback and the fail-open when there is none, auto-on at startup/resume/clear but not compaction, the CCGM_ADVISOR_AUTO opt-out, legacy-file migration, SessionEnd removal, and garbage collection.

Deferred by design (v1)

  • Read-cap on orchestrator context (baton's cost guard: deny oversized main-agent reads, delegate to a scout) — worth revisiting once the mode proves out; skipped so the mode never fights ordinary conversation.
  • Requirements-Ledger / Stop guards — CCGM's plan/progress artifacts and etp's completion contract already cover run-to-completion.
  • Model-conditional auto-enable — every session now starts in the mode; keying the default off the session's model (Fable/Opus only) is a possible follow-up.

Will install

Path Action Target Type
commands/advisor.md → commands/advisor.md command
rules/advisor-mode.md → rules/advisor-mode.md rule
hooks/advisor-guard.py → hooks/advisor-guard.py hook
hooks/advisor-posture.py → hooks/advisor-posture.py hook
hooks/advisor-session-start.py → hooks/advisor-session-start.py hook
hooks/advisor-session-end.py → hooks/advisor-session-end.py hook
settings.partial.json merge settings.json config

Dependencies

Required by

No other module depends on this one.

Included in presets

Install this module

Agent prompt

Recommended for agent users -- hands the whole install off to your assistant.

Fetch https://ccgm.dev/modules/advisor-mode.md and install this module into my Claude Code setup.

Native plugin marketplace

One command via the native plugin marketplace -- additive, does not merge settings.json.

claude plugin install advisor-mode@ccgm

The marketplace path is additive, not a replacement: it installs commands, agents, and skills as native plugin components, but it does not perform the bash installer's deep settings.json merge, and it does not write the always-loaded global CLAUDE.md context. Rules are only injected via an opt-in SessionStart hook rather than being auto-loaded. Use the bash installer when those pieces matter to you.

Manual, per file

Full control -- copy exactly the files you want from the sections below.

Files

Files

1 further file is available as raw text.

rule (1)

rules/advisor-mode.md

# Advisor Mode: The Orchestrator Delegates, It Does Not Implement

While advisor mode is on, the main agent produces specs, reviews, and decisions, never diffs. Implementation goes to cheaper agents, the lead reviews spec compliance and quality, and fixes are delegated until the work is complete, follow-ups included. The posture is mechanical: while this session's mode flag exists, a PreToolUse hook (`advisor-guard.py`, exit 2, bypass-surviving) blocks the main agent's file edits and non-orchestration Bash. Subagent tool calls pass untouched.

The gate is hard rather than advisory because prompt-only "you never implement" postures drift into hands-on patching at friction moments, an integration mismatch or a "one-liner" fix, and capability removal is what held in every documented case. A denial is steering: delegate the mutation, never find a shell trick around it.

## Session State

- The flag is `~/.claude/advisor-mode/<session_id>`; this rule binds only while the running session's own flag exists.
- A SessionStart hook creates the flag, so every fresh, resumed, or cleared session starts in advisor mode. Opt out with `CCGM_ADVISOR_AUTO=false` in the environment or `~/.claude/.ccgm.env`.
- Compaction never re-creates a flag the session removed, so `/advisor off` survives it. SessionEnd drops the flag; SessionStart sweeps flags whose session is gone or whose transcript is untouched for three days, so a live session idle that long loses its gate and needs `/advisor on` again.
- `/advisor` toggles this session; `on|off|status` are also accepted.

## What the Orchestrator Does and Never Does

| Does (latent work) | Never does (delegated work) |
|---|---|
| Decompose work, write specs, define acceptance criteria | Edit or write source files |
| Dispatch and route agents; pick models per the ladder | Run builds, tests, or scripts itself |
| Personally review spec and quality; triage findings on evidence | Commit, push, stash, or apply patches |
| Merge reviewed and green PRs; manage issues, branches, worktrees | "Quick" inline fixes on a PR branch |
| Synthesize results; converse; answer questions directly | Bulk mechanical operations |

Trivial or conversational turns are answered directly. The mode governs the production of work, not thinking.

## The Loop

1. **Route.** Plan-shaped or investigated-issue-shaped work goes through `/etp`, which runs this loop at full ceremony. The steps below are the collapsed loop for ad-hoc work.
2. **Spec.** Objective, context (file paths, line ranges), constraints, deliverable, the why, and explicit acceptance criteria including what must still fail. Copy any safety-critical session constraints in verbatim; subagents do not inherit them.
3. **Dispatch** an `implementer` (sonnet by default) with `isolation: "worktree"`. Parallel units follow the concurrency caps in `subagent-patterns.md`. Delegation depth stays at one.
4. **Review personally**, spec compliance first, then code quality, from the spec, the diff, and fresh verification output. The implementer's rationale is not proof. Delegate builds and tests to a verifier and inspect its actual outputs. `--light-review` in ETP selects spec-only review; the two-stage review is the default.
5. **Triage** supported findings and dispatch fixes. Three fix rounds are the normal checkpoint; more needs new evidence and a viable next check. Cross-provider review is opt-in (`--cross-provider` or an explicit request); a stopped optional review is never approval.
6. **Merge** only reviewed and CI-green work, then tear down the unit's worktree. Follow-ups get the same treatment as first-class units.

## Delegation Ladder and Floor

| Tier | Work |
|---|---|
| haiku | Mechanical: bulk reads and recon, renames, extraction, tabulation, status checks |
| sonnet (default) | Implementation, tests, research; delegated reviews when requested |
| opus | Units that need frontier reasoning: architecture, security review, hard debugging |
| orchestrator | Specs, personal review, routing, triage, adjudication, synthesis |

A subagent spawn costs real fixed overhead (the full rules and CLAUDE.md context loads into it). Do not delegate work smaller than that overhead: batch small related items into one dispatch, and treat trivial textual work (answering, summarizing) as conversation. Never scale agent count when you can scale items per agent. Delegation buys context protection, orchestrator longevity, and parallelism; cost savings are modest, and micro-delegation is net-negative.

## Escape Hatches

- `/advisor off` ends the mode; the right answer when the user asks the orchestrator to implement directly.
- `ADVISOR_DIRECT=1`, in the environment or inline on one Bash command, is a one-off for a deliberate exception ("just fix it yourself"). Do not leave it exported.

## Enforcement Mechanics

- The guard tells main-agent from subagent calls by the hook input's `agent_id`/`agent_type` fields, which subagent calls carry and main-agent calls do not. Drift is asymmetric: if main-agent inputs gained the fields the guard would go inert (fail open); if subagent inputs lost them, subagents would be denied, loudly and recoverably with `/advisor off`.
- The session is identified by the hook input's `session_id`, then `CLAUDE_CODE_SESSION_ID`. A call carrying neither is allowed, the same fail-open choice.
- File writes are allowed only to orchestrator work-product paths: `~/.claude/`, temp and scratchpad roots, `~/code/plans/`, `~/code/docs/`, worktree checkouts, and plan-mode plan files. Trusted policy code is excluded even when reached through a writable-looking symlink.
- Bash is default-deny. Allowed: read-only inspection, read-only git plus branch, worktree, and pull lifecycle, and `gh` PR, issue, run, and label management including merge; redirection and scratch file-ops only into the allowed write roots; dev-tool version and identity probes (`node -v`, `wrangler whoami`) but no other arguments to those binaries. Grouping tokens are structure and their contents are checked as ordinary segments. `$(...)` and backtick substitution pass when every inner command is allowlisted (checked recursively, depth-capped, backtick bodies unescaped first), but a substitution's output may only feed a read-only command. Shells, interpreters, process substitution, and wrapper commands (`env`, `xargs`) are denied outright, except the installed cross-agent-review policy shim and its enumerated actions (that helper records checks and never executes its argv) and the installed `$HOME/.claude/bin/ccgm-persist` CLI with its start, status, done, and cancel grammar (it writes only under `~/.claude/persist/`); the installed `$HOME/.claude/bin/` output CLIs `ccgm-prose-lint`, `xplan-depgraph`, and `ccgm-output-metrics` (read-only, any arguments) and `ccgm-md-page`, allowed only when its output path (`-o OUT`, else the input with an `.html` suffix) is an `.html` path that passes the write-root check. Each of these installed CLIs and the policy shim is trusted only as the entire command (one segment, no substitution) and only when its path is absolute, starts with an unquoted `~/`, or starts with an unquoted or double-quoted `$HOME/`; a hook error denies instead of passing.
- The guard performs quote removal (including `$'…'` and `$"…"`), backslash-escape removal, and substitution of variables it can resolve before checking a word, so a flag cannot hide behind quoting. What it cannot model it denies: brace groups, unquoted globs, an unquoted expansion it cannot resolve anywhere in a word, a resolvable one whose value carries whitespace or a glob character, and undecodable `$'…'` escapes are denied for any command that is not read-only, and always in a redirect target or scratch-op path. A double-quoted expansion is allowed after a flag's first `=` (so `--title="$T"` works) and denied leading or before it. `$_` always counts as unresolvable. `gh api` mutation flags match as prefixes, and any single-dash `git branch` cluster containing `d D m M f c C` counts as a mutator.
- Open gaps: `awk` bodies and heredoc content can smuggle writes; a relative path after a `cd` is denied as unknowable; `~+`, `~-`, and `~N` are denied outright; backticks inside a double-quoted markdown body read as substitution, so pass bodies with `--body-file`. Over-denial is the accepted direction, and each denial names the delegation recipe.

See `subagent-patterns.md` for the spec format, two-stage review, status protocol, and concurrency caps; the `git-worktrees` skill for worktree lifecycle; `verification.md` for what counts as evidence.
command (1)

commands/advisor.md

---
description: Toggle advisor mode for THIS session - the orchestrator delegates implementation to cheaper agents and reviews their work; a hard PreToolUse gate blocks its own edits while the mode is on
argument-hint: "[on | off | status] (bare /advisor toggles this session)"
---

# /advisor - Advisor Mode Toggle

Put this session into (or take it out of) **advisor mode**: the main agent
becomes a pure orchestrator that specs, delegates, reviews, and merges — and is
mechanically blocked from implementing directly. The full contract lives in
`~/.claude/rules/advisor-mode.md`.

State is **one flag file per session**: `~/.claude/advisor-mode/<session_id>`.
Every command below acts on the current session only; other sessions keep
whatever mode they are in. Four hooks key on it: `advisor-guard.py`
(PreToolUse, exit-2 hard gate on main-agent file edits and non-orchestration
Bash), `advisor-posture.py` (UserPromptSubmit, injects the posture and this
session's id each turn), `advisor-session-start.py` (creates the flag at
session start and garbage-collects dead sessions' flags), and
`advisor-session-end.py` (removes the flag when the session ends).

## Usage

```
/advisor            # TOGGLE this session: on if off, off if on
/advisor on         # enable explicitly (idempotent)
/advisor off        # disable explicitly (idempotent)
/advisor status     # this session's state, plus every session in the mode
```

## Workflow

**First, resolve this session's id.** Everything below needs it:

1. Run `echo $CLAUDE_CODE_SESSION_ID`. A non-empty value is the id.
2. If it is empty, use the session id from the advisor-mode posture text
   injected into this turn (it names the id and the flag path).
3. If neither is available, report that the session id could not be resolved
   and stop. Never fall back to a machine-global flag file — that is the bug
   this layout removed.

The commands below use `$CLAUDE_CODE_SESSION_ID`; substitute the literal id
when you got it from the posture text instead. Do not wrap any of them in
`$(...)` or backticks — the guard denies command substitution outright.

Then parse the argument:

- **No argument (the default) — toggle.** Check this session's flag:
  `test -f ~/.claude/advisor-mode/$CLAUDE_CODE_SESSION_ID` → present means run
  the `off` branch; absent (exit 1) means run the `on` branch. The explicit
  verbs exist for scripting and for saying exactly what you mean; the bare
  command never requires them.
- **`on`** — if the flag already exists, say so and change nothing (rewriting
  it would lose the original timestamp). Otherwise create it:

  ```
  mkdir -p ~/.claude/advisor-mode
  date -u +'on %Y-%m-%dT%H:%M:%SZ' > ~/.claude/advisor-mode/$CLAUDE_CODE_SESSION_ID
  ```

  (the guard allowlists writes under `~/.claude/`, so this passes even while
  the mode is on). Confirm in two lines: the mode is on for this session, and
  the one-line posture — "I orchestrate: specs, delegation, personal spec/quality review,
  triage, merge. Implementation goes to subagents; `/advisor off` ends the
  mode."
- **`off`** — `rm -f ~/.claude/advisor-mode/$CLAUDE_CODE_SESSION_ID`. Confirm:
  `Advisor mode off for this session.`
- **`status`** — report this session first, then the machine:
  `cat ~/.claude/advisor-mode/$CLAUDE_CODE_SESSION_ID` (on, with the timestamp
  it started) or note that it exits 1 when the mode is off. Then list every
  session currently in the mode with its start timestamp:

  ```
  grep -H '' ~/.claude/advisor-mode/*
  ```

  and mark which line is this session. `-H` is load-bearing — `grep` prints
  the filename only when it is given more than one file, and exactly one
  session in the mode is the common case, so without it the output has no
  session id to mark. When no session is in the mode the glob does not expand
  and the command reports `No such file or directory`: that is "no sessions in
  advisor mode", not an error to report as a failure.

Then stop — the toggle is the whole command. Do not begin decomposing or
delegating anything until the user gives actual work.

## Notes

- **The mode is per session.** Turning it on or off here never touches another
  running session. A fresh, resumed, or cleared session starts in advisor mode
  by default; opt out with `CCGM_ADVISOR_AUTO=false` in the environment or in
  `~/.claude/.ccgm.env`. Only compaction is exempt: it never re-enables a mode
  the session turned off, but a resume or a `/clear` does start the mode again,
  so `/advisor off` lasts until the session restarts.
- **Legacy state, and other ways the state directory can be missing.** The mode
  used to be a single file at `~/.claude/advisor-mode`. The SessionStart hook
  deletes that file — and a symlink standing in the same place — on first run.
  If `status` reports off in a session that never turned it off, check that
  `~/.claude/advisor-mode` is a writable directory: a leftover file, a symlink,
  or a permission problem each disables the auto-on silently, and the hook
  cannot report it (SessionStart hooks write no output). `rm
  ~/.claude/advisor-mode` once, or fix the permissions, and start a new
  session.
- Flags outlive nothing: the SessionEnd hook removes this session's flag, and
  SessionStart sweeps flags whose session is gone.
- One-off escape hatch: `ADVISOR_DIRECT=1` (env or inline on a Bash command) —
  for user-directed exceptions only.
- If the user asks for advisor mode but then tells you to implement something
  directly, the honest responses are `/advisor off` first, or a one-off
  `ADVISOR_DIRECT=1` — not shell workarounds while the gate is up.
hook (4)

hooks/advisor-guard.py

#!/usr/bin/env python3
"""
PreToolUse hook that HARD-BLOCKS direct implementation by an advisor-mode
orchestrator.

Why: an expensive orchestrator session (Fable/Opus) in advisor mode delegates
implementation to cheaper agents and reviews their work. Advisory-only "you
never implement" prompts demonstrably fail under pressure — the documented
failure mode is the orchestrator drifting into hands-on patching exactly at
friction moments (integration fixes, small "one-liners"). This gate makes the
posture mechanical: while this session's flag file exists, the MAIN agent
cannot mutate source; a denial is steering ("delegate this"), not an obstacle.

Classification: bypass-retained. Denials use exit 2 (hard_block semantics,
inlined so the hook is dependency-free). A JSON `permissionDecision: den…

View raw (74355 bytes)

hooks/advisor-posture.py

#!/usr/bin/env python3
"""
UserPromptSubmit hook: while advisor mode is on for THIS session, remind each
turn that the session is an orchestrator.

State is per session: the flag is ~/.claude/advisor-mode/<session_id>, read
from the hook input's session_id (CLAUDE_CODE_SESSION_ID as the fallback), so
another session's mode never injects here. The injection also names the
session id, which is what /advisor uses to find its own flag when the
environment variable is missing.

The guard (advisor-guard.py) enforces the posture mechanically; this injection
explains it so the model delegates instead of fighting denials. Adapted from
baton's orchestrator-mode injection pattern. One JSON parse and one stat()
per prompt when the mode is off.
"""

import json
import os
import re
import sys

SESSION_ID_ENV = "CLAUDE_CODE_SESSION_ID"

# Session ids are uuids; anything else cannot name a flag file. Rejecting
# separators and dot-entries keeps the flag inside the state directory.
SESSION_ID_RE = re.compile(r"[A-Za-z0-9._-]+")

CONTEXT = (
    "advisor mode is ON — you are the orchestrator: a PreToolUse hook blocks "
    "your direct file edits and non-orchestration Bash. Your deliverable is "
    "the spec, not the diff: decompose the work, dispatch implementer "
    "subagents (four-field spec with file paths and the objective's why; "
    "isolation: worktree; sonnet by default, haiku for mechanical work, opus "
    "for hard units), personally review spec compliance and quality with "
    "explicit success criteria, run normal repository checks, and delegate fixes "
    "(max 3 rounds, then escalate). Cross-provider review is opt-in and bounded; a stopped optional review is never approval. Route plan- or issue-shaped work through "
    "/etp. Batch small related items into one dispatch — a subagent spawn "
    "costs real overhead — and copy any safety-critical session constraints "
    "into every spec, because subagents do not inherit them. Trivial or "
    "conversational turns: just answer directly. A guard denial means "
    "delegate it, never shell-trick around it. /advisor off ends the mode."
)

TRAILER = (
    " The mode is per session: this session's id is {sid}, and its flag is "
    "~/.claude/advisor-mode/{sid}. Other sessions are unaffected."
)


def session_id(data):
    """This turn's session id: hook input first, environment as fallback."""
    for candidate in (data.get("session_id"), os.environ.get(SESSION_ID_ENV)):
        if not isinstance(candidate, str):
            continue
        candidate = candidate.strip()
        if candidate in (".", "..") or not SESSION_ID_RE.fullmatch(candidate):
            continue
        return candidate
    return None


def main():
    try:
        data = json.load(sys.stdin)
    except Exception:
        data = {}
    if not isinstance(data, dict):
        data = {}
    sid = session_id(data)
    if not sid:
        sys.exit(0)
    flag = os.path.join(
        os.path.expanduser("~"), ".claude", "advisor-mode", sid)
    if not os.path.isfile(flag):
        sys.exit(0)
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "UserPromptSubmit",
            "additionalContext": CONTEXT + TRAILER.format(sid=sid),
        },
        "suppressOutput": True,
    }))
    sys.exit(0)


if __name__ == "__main__":
    main()

hooks/advisor-session-start.py

#!/usr/bin/env python3
"""
SessionStart hook: put THIS session into advisor mode, and clean up flags left
behind by sessions that are gone.

Advisor-mode state is one flag file per session,
`~/.claude/advisor-mode/<session_id>`, so one session's mode never binds
another's. This hook owns three jobs, in order:

  1. Migration — the mode used to be a single regular file at
     `~/.claude/advisor-mode`. If that file (or a symlink where the
     directory belongs) is still there, delete it; the path becomes the
     state directory. Leaving it would be silent and total: makedirs,
     listdir and the flag write all fail, and every session runs un-gated.
  2. Garbage collection — a session that crashed never ran SessionEnd, so its
     flag would sit there forever. A flag whose session has no transcript
     under `~/.claude/projects/*/<session_id>.jsonl` is removed once it is
     more than an hour old (the grace period keeps a session that started
     seconds ago, and has not written its transcript yet, from being swept).
     A flag whose transcript has not been touched in three days is removed
     too — including a session that is still alive but has been idle that
     long, which loses its gate until it runs `/advisor on` again (recorded
     as a known gap in rules/advisor-mode.md). The current session's flag is
     never touched; when no session id resolves there is no current session
     to exempt, so GC runs over every flag, which only reaches flags the
     rules above would sweep anyway.
  3. Auto-on — create this session's flag, so every session starts in advisor
     mode. Opt out with CCGM_ADVISOR_AUTO=false in the environment or in
     `~/.claude/.ccgm.env` (environment wins; unset means on), matching
     CCGM_AUTO_UPDATE_CHECK. GC still runs when the
     auto-on is opted out.

`source == "compact"` skips the auto-on: compaction is mid-session continuity,
and re-creating the flag there would silently undo an explicit `/advisor off`.
An existing flag is never rewritten, so a resume keeps its original timestamp.

Never raises, never writes to stdout (the per-turn posture injection is
advisor-posture.py's job), always exits 0.
"""

import glob
import json
import os
import re
import sys
import time

SESSION_ID_ENV = "CLAUDE_CODE_SESSION_ID"
AUTO_ENV = "CCGM_ADVISOR_AUTO"
FALSE_VALUES = ("false", "0", "no", "off")

# Session ids are uuids; anything else cannot name a flag file. Rejecting
# separators and dot-entries keeps the flag inside the state directory.
SESSION_ID_RE = re.compile(r"[A-Za-z0-9._-]+")

NO_TRANSCRIPT_GRACE_SECONDS = 60 * 60          # 1 hour
STALE_TRANSCRIPT_SECONDS = 3 * 24 * 60 * 60    # 3 days


def home():
    return os.path.expanduser("~")


def state_dir():
    return os.path.join(home(), ".claude", "advisor-mode")


def session_id(data):
    """This session's id: hook input first, environment as fallback."""
    for candidate in (data.get("session_id"), os.environ.get(SESSION_ID_ENV)):
        if not isinstance(candidate, str):
            continue
        candidate = candidate.strip()
        if candidate in (".", "..") or not SESSION_ID_RE.fullmatch(candidate):
            continue
        return candidate
    return None


def auto_on_enabled():
    """CCGM_ADVISOR_AUTO: environment wins over .ccgm.env; unset means on.

    This is the project's first opt-OUT flag in `.ccgm.env`, so the parser is
    deliberately more tolerant than the opt-in flags' `startswith("KEY=")`:
    a line the operator meant as "off" that fails to parse turns the mode ON
    against their wish. `export`, a trailing comment, spaces around `=`, and
    quotes are all accepted, and the last matching line wins, as a shell
    would read the file.
    """
    value = os.environ.get(AUTO_ENV)
    if isinstance(value, str) and value.strip():
        return value.strip().lower() not in FALSE_VALUES
    setting = None
    env_file = os.path.join(home(), ".claude", ".ccgm.env")
    try:
        with open(env_file) as handle:
            for line in handle:
                line = line.split("#", 1)[0].strip()
                if line.startswith("export "):
                    line = line[len("export "):].strip()
                key, sep, raw = line.partition("=")
                if sep and key.strip() == AUTO_ENV:
                    setting = raw.strip().strip("'\"").lower()
    except (OSError, IOError):
        pass
    if setting is None:
        return True
    return setting not in FALSE_VALUES


def migrate_legacy_file(directory):
    """The pre-per-session state was a regular file at this exact path.

    A symlink is removed too: unlinking it never touches its target, and a
    symlink standing where the state directory belongs is the same silent
    dead end as the legacy file (makedirs raises, listdir raises, the flag
    write raises, and the mode is off everywhere with no signal).
    """
    try:
        if os.path.islink(directory) or os.path.isfile(directory):
            os.remove(directory)
    except OSError:
        pass


def collect_garbage(directory, current_sid):
    """Drop flags whose session is gone. Never touches the current session."""
    now = time.time()
    try:
        names = os.listdir(directory)
    except OSError:
        return
    for name in names:
        if name == current_sid:
            continue
        flag = os.path.join(directory, name)
        try:
            if not os.path.isfile(flag):
                continue
            transcripts = glob.glob(os.path.join(
                home(), ".claude", "projects", "*", name + ".jsonl"))
            if not transcripts:
                # A session that just started may not have a transcript yet.
                if now - os.path.getmtime(flag) > NO_TRANSCRIPT_GRACE_SECONDS:
                    os.remove(flag)
                continue
            newest = max(os.path.getmtime(t) for t in transcripts)
            if now - newest > STALE_TRANSCRIPT_SECONDS:
                os.remove(flag)
        except OSError:
            continue  # a racing writer or an unreadable entry: leave it


def enable(directory, sid):
    """Create this session's flag. An existing flag keeps its timestamp."""
    flag = os.path.join(directory, sid)
    if os.path.exists(flag):
        return
    stamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
    try:
        with open(flag, "w") as handle:
            handle.write("on %s\n" % stamp)
    except (OSError, IOError):
        pass


def main():
    try:
        data = json.load(sys.stdin)
    except Exception:
        data = {}
    if not isinstance(data, dict):
        data = {}

    directory = state_dir()
    migrate_legacy_file(directory)
    try:
        os.makedirs(directory)
    except OSError:
        pass  # already there, or it cannot be created — nothing else to do

    sid = session_id(data)
    collect_garbage(directory, sid)

    if not sid:
        return
    if data.get("source") == "compact":
        return
    if not auto_on_enabled():
        return
    enable(directory, sid)


if __name__ == "__main__":
    try:
        main()
    except Exception:
        pass  # a session must never fail to start because of this hook
    sys.exit(0)

hooks/advisor-session-end.py

#!/usr/bin/env python3
"""
SessionEnd hook: drop this session's advisor-mode flag.

Advisor-mode state is one flag file per session,
`~/.claude/advisor-mode/<session_id>`. Removing it when the session ends keeps
the state directory from filling with dead sessions. A session that dies
without firing SessionEnd is caught later by advisor-session-start.py's
garbage collection.

Never raises, always exits 0.
"""

import json
import os
import re
import sys

SESSION_ID_ENV = "CLAUDE_CODE_SESSION_ID"

# Session ids are uuids; anything else cannot name a flag file. Rejecting
# separators and dot-entries keeps the flag inside the state directory.
SESSION_ID_RE = re.compile(r"[A-Za-z0-9._-]+")


def session_id(data):
    """This session's id: hook input first, environment as fallback."""
    for candidate in (data.get("session_id"), os.environ.get(SESSION_ID_ENV)):
        if not isinstance(candidate, str):
            continue
        candidate = candidate.strip()
        if candidate in (".", "..") or not SESSION_ID_RE.fullmatch(candidate):
            continue
        return candidate
    return None


def main():
    try:
        data = json.load(sys.stdin)
    except Exception:
        data = {}
    if not isinstance(data, dict):
        data = {}
    sid = session_id(data)
    if not sid:
        return
    flag = os.path.join(
        os.path.expanduser("~"), ".claude", "advisor-mode", sid)
    try:
        os.remove(flag)
    except OSError:
        pass  # already gone, or never created


if __name__ == "__main__":
    try:
        main()
    except Exception:
        pass
    sys.exit(0)
config (1)

settings.partial.json

Merged into ~/.claude/settings.json -- a fragment, not a replacement.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|MultiEdit|Write|NotebookEdit|mcp__filesystem__write_file|mcp__filesystem__edit_file|mcp__filesystem__move_file|Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 $HOME/.claude/hooks/advisor-guard.py",
            "timeout": 5000
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 $HOME/.claude/hooks/advisor-posture.py",
            "timeout": 5000
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "matcher": "startup|resume|clear",
        "hooks": [
          {
            "type": "command",
            "command": "python3 $HOME/.claude/hooks/advisor-session-start.py",
            "timeout": 5000
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 $HOME/.claude/hooks/advisor-session-end.py",
            "timeout": 5000
          }
        ]
      }
    ]
  }
}