SLOPSHOPPER

dogma

Intelligent sync of Claude instructions with enforcement hooks for security and consistency

newbandguardtoastprocesstimer
A shopper browsing a rack in a slop shop
README

dogma

Intelligent sync of Claude instructions from any source, with enforcement hooks for security and consistency. Use the opinionated default rules or bring your own.

Why dogma?

Claude Code is powerful, but without guardrails it's YOLO mode - and many developers (or their employers) don't want that.

For Teams and Enterprises

Single Source of Truth: Define your coding standards, security rules, and AI guidelines once. Use the default source repository or create your own - every team member syncs from the same source, ensuring consistency across the organization.

Custom Rules, Your Way: dogma doesn't force you to use anyone's defaults. Point it at your own private repository with your company's specific guidelines. The only requirement: a similar structure to the default source repository.

No AI Traces in Code: Many companies prohibit AI-generated artifacts in their codebase. dogma's hooks and cleanup commands help detect and remove typical AI patterns (curly quotes, em-dashes, AI phrases) before they reach your commits.

Smart Merging: When syncing rules, dogma doesn't blindly overwrite your local customizations. It handles merging intelligently, so project-specific rules stay intact while shared standards get updated.

For Individual Developers

Sync Across Projects: Same rules everywhere. Whether you have 5 or 50 repositories, one /dogma:sync keeps them all aligned with your personal standards.

Sync Across Devices: Work on multiple machines? Your rules live in a source repository - sync them wherever you need them.

Continuously Maintained

The default source repository is actively developed and used by the author across all personal and professional projects. Strong self-interest ensures it stays functional and up-to-date.

Features

Slash Commands

  • /dogma:sync - Sync Claude instructions from any source with interactive review
  • /dogma:cleanup - Find and fix AI-typical patterns in code
  • /dogma:lint - Project-agnostic linting and formatting on staged files (non-interactive)
  • /dogma:lint:setup - Interactive setup for linting/formatting tools
  • /dogma:versioning - Check and sync version numbers across all config files; at a release it assembles a versioned changelog.d/ into the CHANGELOG (no tag)
  • /dogma:permissions - Create or update DOGMA-PERMISSIONS.md interactively
  • /dogma:force - Interactively collect and apply CLAUDE rules to the project
  • /dogma:sanitize-git - Sanitize git history from Claude/AI traces and fix tracking issues
  • /dogma:docs-update - Sync documentation across README files and wiki articles
  • /dogma:ignore - Add ignore patterns to multiple locations at once (.gitignore, .git/info/exclude)
  • /dogma:ignore:audit - Show which AI patterns are missing from ignore files
  • /dogma:ignore:sync-all - Sync AI patterns from sync.md to all local repos (marketplace only)
  • /dogma:recommended:setup - Check and install recommended plugins and MCP servers from a source

Permissions System

Control Claude's autonomy with DOGMA-PERMISSIONS.md in your project root:

<permissions>
- [x] (§6gpt) May run `git add` autonomously      # auto
- [x] (§2w1t) May run `git commit` autonomously   # auto
- [?] (§bww9) May run `git push` autonomously     # ask first
- [?] (§0lgy) May delete files autonomously       # ask first
</permissions>
MarkerModeBehavior
[x]autoClaude does it automatically
[?]askClaude asks for confirmation
[ ]denyBlocked (manual only)

Run /dogma:permissions to configure interactively.

Stable setting ids: every setting carries a fixed id (§xxxx) (4 lowercase base36 chars) right after its checkbox, the same in every repo; parsed headings carry it too (### Test Commands (§ly5v), Worktree files (§47p9) ...). dogma and credo find a setting by its id first, anywhere in the <permissions> block, so the text may be reworded, translated or merged by /dogma:sync without silently losing the setting. Only when no line carries the id they fall back to the old heading + text match, so files without ids keep working. Keep the id when editing a line. The full list is in docs/permission-ids.md; scripts pass a spec "§xxxx|text pattern" to get_permission_mode / check_permission in scripts/lib-permissions.sh.

Which DOGMA-PERMISSIONS.md applies (and inheritance): a session is often started in a parent or workspace folder while the work happens in another repo. dogma therefore picks the file in this order: (1) the target of the action - a Bash command's git -C <dir> ..., a leading cd <dir> && ... / cd <dir>; ..., or the edited file's own path; (2) the credo pinned project (/credo:project), when credo is installed; (3) the current folder (normally the folder the session was started in). From there the nearest DOGMA-PERMISSIONS.md upward counts (for a linked worktree also the main checkout). The checkbox - [x] (§r3nx) inherit permissions at the top of the <permissions> block (under ## Inheritance; a file without it behaves like [x]) makes every setting the file does not define come from the session folder's file; settings defined in the file always win. Example: the session starts in ~/workspace (with its own DOGMA-PERMISSIONS.md) and Claude runs git -C ~/workspace-projects/app commit - app's file applies, and every setting app's file lacks comes from ~/workspace's file; with [ ] only app's file counts. Inheritance goes per setting id (lines without ids per text, within each file), per Test Commands stage, and for the Worktree files list. The session folder is recorded at session start by the session-dir-record.sh SessionStart hook (per session id under ${CLAUDE_CONFIG_DIR:-~/.claude}/dogma/session-dirs/, found again through $CLAUDE_CODE_SESSION_ID; records older than 30 days are pruned), so inheritance still works when a Bash command ran cd <project> && ... first; without a record, or when the recorded folder is gone, $PWD counts as before.

scripts/permissions-summary.sh [--json] [dir] lists only the restricting entries ([?] ask, [ ]/[0] deny) of the permission sections of the applicable DOGMA-PERMISSIONS.md (effective view including inherited entries; read-only, exit 4 when none is found), so any renderer can show them without parsing the file. dir is the target (default: pinned project, else the current folder). The JSON output marks inherited entries under an optional "source": {"<label>": "<file>"} key. Checkboxes under a ## Workflow ... or ## Inheritance heading are on/off switches ([ ] means off, not deny) and are skipped. Ids (§xxxx) never show up in its labels.

Worktrees (Hydra subsection): a fresh git worktree only has versioned files, so excluded ones (rules, credo items, local config) are missing there. The ### Hydra subsection can carry a "Worktree files" list - - link: CLAUDE.md (symlink to the main checkout, the default kind; a line without a kind is a link) or - copy: .env.local (separate copy per worktree). scripts/worktree-files.sh [--json] [dir] prints the effective list as <kind> <path> lines (exit 0, 1 bad args); when the list is missing or empty it prints the default: link CLAUDE.md, CLAUDE/, GUIDES/, DOGMA-PERMISSIONS.md and .credo/. hydra's worktree-setup.sh and credo's credo-worktree-setup.sh apply it right after git worktree add (skipping missing and versioned paths and untracked paths that are not ignored in the main checkout, never overwriting) - so list only excluded/ignored paths. The checkbox [x] clean up merged worktrees automatically (default [x]; [?] asks each time, [ ] never, a file without it behaves as [?]) lets credo remove merged and clean worktrees at item close; with credo, [x] use Hydra for 2+ independent tasks makes credo use hydra's flow automatically for parallel code tracks. These Workflow lines do not change the permissions-summary.sh output.

Test commands (optional): a ### Test Commands subsection under ## Workflow Permissions says WHICH command runs at which stage (the Testing / Final Verification checkboxes say WHEN), language-agnostic, one line per stage: ` - commit: npm run lint , - all [main, stage]: npm test . Stages: commit (fast static checks before every commit), push (before git push), relevant (tests for the changed code when an item is reported done; takes no branch filter), build (build check), all (full suite at integration into a filtered branch and in Final Verification). The optional [branch, ...] filter limits a stage to those branches. Every line is optional - a missing line or section means Claude decides as before. dogma never runs them itself: scripts/test-commands.sh [--json] [dir] lists them and scripts/test-commands.sh get <stage> [branch] [--dir dir] prints the command that applies (exit 4 when none does), so Claude or other tools run it at the stage. The Final Verification checkbox run ALL tests only at release (default off) skips all` on each merge and runs it once in the release commit, the normal commit that bundles several items with the version bump (never a tag or hosted release; those stay with the user).

Changelog fragments (changelog.d/): when the repo has a versioned changelog.d/ at its root, /dogma:versioning assembles the fragments at release - the bundling commit with the version bump, never a tag. It prepends a ## [X.Y.Z] - YYYY-MM-DD section built from the fragments to the existing CHANGELOG, keeping the repo's existing heading and subsection format (it asks when the format is unclear), and removes the consumed fragments in the same commit. Language-agnostic; without a versioned changelog.d/ nothing changes.

Update notices

When a dogma update adds something you should act on (for example a new section in DOGMA-PERMISSIONS.md), you are told once per repo - nobody has to remember to look. Relevance has two layers: the plugin author only adds an entry to notices.json for changes that need user action (most version bumps add none), and each entry's applies script decides whether it fits THIS repo; repos where it does not apply never see it.

At session start the notices-inject.sh hook tells Claude about pending notices. Claude asks you at the first natural pause (Ask tool, or plain text without one): Run the action (e.g. /dogma:permissions; marked seen after it completed), Later (asked again next session) or Never (marked seen). Running unattended/autonomously, Claude does not ask and leaves the notice pending. Seen state is per repo and per profile under ${CLAUDE_CONFIG_DIR:-~/.claude}/dogma/notices-seen/. Notices apply wherever an effective DOGMA-PERMISSIONS.md resolves (same resolution as the permission hooks, also in non-git folders) and are keyed to that file's directory - the git toplevel for a repo with its own file, otherwise e.g. the session folder whose file a credo pinned project inherits, so they are shown once across both. The notice mechanism never changes anything in the repo itself. With the band (below) a toast hints at pending notices at session start (visible for 12 s). scripts/notices-pending.sh [--json] [dir] lists them, scripts/notices-pending.sh mark <id> [dir] marks one seen.

Source broadcasts

The owner of a dogma source (the template repo /dogma:sync pulls from) can tell every repo that syncs from it something important once - for example "run /dogma:sync to get the new setting ids" - without anyone having to remember. Only hand-written entries in a NOTICES.md at the source root are announced; there are no generic "files changed" notices.

# Notices

## 2026-10-02 (§n001) Stable setting ids
Action: /dogma:sync
Every setting now carries a fixed id. Run a sync once so this repo gets them.

One ## heading per entry with a date YYYY-MM-DD and an id (§...) (letters, digits, ., _, -; never reuse one); the rest of the heading is the title. An optional Action: line names the command to offer (usually /dogma:sync); the other lines are the text. Entries are parsed id-first: a heading without an id or without a date is ignored, and so is text before the first entry. NOTICES.md itself is never synced into projects.

They are delivered exactly like the plugin's update notices (ids prefixed src:, e.g. src:n001; same Run / Later / Never question, same seen state per repo and profile, the band toast counts both kinds), with these rules:

  • The source is CLAUDE_MB_DOGMA_SOURCE (an https or ssh URL including SSH host aliases like git@github-work:owner/repo.git, a file:// URL, or an absolute local path). Unset means no broadcasts. /dogma:sync asks once for it when it is unset and stores it (global settings by default).
  • URL sources are read through a shallow clone in ${CLAUDE_CONFIG_DIR:-~/.claude}/dogma/source-cache/<hash>/, refreshed with git fetch at most once a day per source, in the background (a session never waits for it; a new entry shows up the session after the fetch). Local paths are read directly. Fetches never prompt and never hang (15 s timeout, no terminal or askpass prompt, ssh BatchMode=yes); your normal git configuration is used as is. With several git accounts routed per folder, the fetch uses the identity routing of the repo the session runs in (its url.*.insteadOf rewrites and core.sshCommand, nothing credential-related), and each identity gets its own cache, daily check and hint.
  • An unreachable source stays silent; at most once a day Claude mentions that the source is not reachable with the current git access (use a URL your git reaches without prompts, e.g. an SSH host alias, or a local path).
  • Only contexts that use dogma get them (an effective DOGMA-PERMISSIONS.md, own or inherited, also in non-git folders; or a CLAUDE/ dir at the git root), never the source repo itself. Entries older than 90 days (CLAUDE_MB_DOGMA_NOTICES_MAX_AGE_DAYS) are skipped so a fresh repo is not flooded.
  • A completed /dogma:sync marks pending source broadcasts whose action is /dogma:sync as seen.

Claude Code band (optional)

When dogma runs inside Claude Code with mods support, hooks/band.tsx (listed under modules in hooks/hooks.json) draws a band above the prompt: ◆ dogma with the restricting entries from permissions-summary.sh, one row block per kind (deny red, ask yellow; all auto when nothing restricts). Changed entries flash for 6 s (moved fuchsia, new white, removed struck through). When a dogma hook blocks a tool call, a ⛔ blocked row with the tool and reason shows for 15 s, plus a toast (6 s). At session start a toast hints at pending update notices and source broadcasts (12 s). With the credo band installed it sits below credo's band and hides at credo's open only preset; without credo it always shows. The band only reads and renders - the enforcement hooks work exactly the same without it and in harnesses without mods.

Enforcement Hooks

  • Git permissions, secrets detection
  • File and search protection, prompt injection detection
  • AI traces validation, language rules reminders
  • Dependency verification (asks before package installs)
  • All hooks toggleable via environment variables

Environment Variables

VariableDefaultDescription
CLAUDE_MB_DOGMA_ENABLEDtrueMaster switch for all hooks
CLAUDE_MB_DOGMA_PRE_COMMIT_LINTtrueBlock git commit until /dogma:lint is run
CLAUDE_MB_DOGMA_SKIP_LINT_CHECKfalseSkip pre-commit lint check (set by Claude after lint)
CLAUDE_MB_DOGMA_AUTO_FORMATtrueAllow automatic formatting of staged files
CLAUDE_MB_DOGMA_LINT_ON_STOPtrueRun lint check when task completes (fallback)
CLAUDE_MB_DOGMA_MODEL_POLICYtrueToggle for model enforcement hook
CLAUDE_MB_DOGMA_FORCE_PARENT_MODELtrueEnforce parent model for all plugins
CLAUDE_MB_DOGMA_BUILTIN_INHERIT_MODELtrueForce built-in agents to inherit parent model
CLAUDE_MB_DOGMA_ALLOW_MODEL_DOWNGRADEfalseAllow explicit model downgrades below parent
CLAUDE_MB_DOGMA_RESET_INTERVAL2Reset interval for subagent enforcement state (number of prompts between resets). Set to 0 to disable enforcement entirely.
CLAUDE_MB_DOGMA_NOTICEStrueTell Claude once per repo about pending update notices and source broadcasts at session start
CLAUDE_MB_DOGMA_SOURCE-Your dogma source: https/ssh git URL (SSH host aliases work), file:// URL or absolute path. Used by /dogma:sync instead of the built-in default and read for source broadcasts (NOTICES.md); /dogma:sync asks once and stores it when unset
CLAUDE_MB_DOGMA_NOTICES_MAX_AGE_DAYS90Skip source broadcasts older than this many days (0 = no limit)
CLAUDE_MB_DOGMA_SOURCE_FETCHbackgroundHow a stale source cache is refreshed: background (never delays a session), sync (wait, max 15 s), off (never fetch)
CLAUDE_MB_DOGMA_TOKEN_ALLOW_DIRS-Comma-separated list of directories whose files skip path-based name checks (content scanning still applies). CLAUDE_PLUGIN_ROOT is always allowed automatically.

Token and File Protection

  • Safe dotenv variants: .env.example, .env.sample, .env.template are excluded from secret detection and git-add protection
  • Grep hook: The Grep tool is blocked from searching in sensitive files (credential files, .env files, key files) - same rules as the Read hook

Delete Guard (always on)

scripts/delete-guard.sh (core: delete-guard.py, python3) blocks Bash commands that would delete, move away or symlink onto protected paths: /, every first-level directory, /home down to depth 2, the home directory and its direct children, and every absolute path outside /tmp/X+ and /var/tmp/X+. Unlike file protection it ignores DOGMA-PERMISSIONS.md, has no per-hook switch and no worktree exemption; only CLAUDE_MB_DOGMA_ENABLED=false turns it off.

  • Targets are resolved before the check: ~ and $HOME are expanded, relative paths are joined to the working directory (including a preceding cd), symlinks are followed, and for a glob the directory before the wildcard is checked. A symlink in /tmp pointing at the home therefore cannot smuggle rm -rf /tmp/link/* through.
  • Strict when unsure: a target that cannot be resolved for sure (command substitution, an unset or re-assigned variable, eval, xargs rm, find -L ... -delete) is blocked. Variables assigned from mktemp in the same command are known-safe.
  • Symlink creation: ln -s, cp -s and interpreter symlink calls onto protected paths are blocked as an extra hurdle.
  • Fail closed: without python3, or on an internal error, a destructive-looking command is denied.
  • Limits: the guard reads the command text before it runs. A script file that deletes on its own, or a symlink swapped between check and execution, is outside what a hook can see.

Usage Warning

The enforcement hooks increase token consumption significantly. Recommended for Claude Max 20x (or minimum Max 5x). For sync-only usage without hooks, any plan works.

Requirements

  • jq - JSON processor (install: sudo apt install jq)

Installation

claude plugin marketplace add Marcel-Bich/marcel-bich-claude-marketplace
claude plugin install dogma@marcel-bich-claude-marketplace

Documentation

Full documentation, hook details, configuration, and customization options:

View Documentation on Wiki

License

MIT - See LICENSE for full terms.


Claude Code, Claude Code Plugin, Claude Code Extension, Claude Code Hooks, Claude Code Rules, Claude Code Enforcement, Claude Code Security, Claude Code Git, Claude Code Secrets, Claude Code Dependencies, Claude Code AI Traces, Claude Code Prompt Injection, Claude Code Instructions, Claude Code CLAUDE.md, Claude Code Configuration, Claude Code Settings, Claude Code Customization, Claude Code Workflow, Claude Code Automation, Claude Code Best Practices, Claude Code Guidelines, Claude Code Standards, Claude Code Conventions, Claude Code Linting, Claude Code Validation, Claude Code Protection, Claude Code Safety, Claude Code Guard, Claude Code Filter, Claude Code Block, Claude Code Warn, Claude Code Remind, Claude Code Sync, Claude Code Merge, Claude Code Import, Claude Code Export, Anthropic CLI, Anthropic Plugin, Anthropic Extension, Anthropic Claude, Anthropic AI, AI Agent Rules, AI Agent Guidelines, AI Agent Instructions, AI Agent Configuration, AI Agent Customization, AI Code Assistant, AI Coding, AI Programming, AI Development, LLM Rules, LLM Guidelines, LLM Instructions, LLM Configuration, Git Hooks, Git Protection, Git Security, Git Secrets, Git Credentials, Git Add Protection, Git Commit Protection, Git Push Protection, Secret Detection, Credential Detection, API Key Protection, Environment Variables, Prompt Injection Detection, Prompt Injection Protection, Prompt Injection Guard, AI Traces Detection, AI Traces Removal, Git History Sanitization, Git History Cleanup, Force Push, Filter Branch, Curly Quotes, Em Dashes, Smart Quotes, Typography Cleanup, German Umlauts, Language Rules, Code Quality, Code Standards, Code Conventions, Code Review, Pre-commit Hooks, Post-commit Hooks, UserPromptSubmit, PreToolUse, PostToolUse, Stop Hook, Marcel Bich, marcel-bich-claude-marketplace, dogma plugin, rules enforcement plugin

Source 2 files
hooks/band.tsx 269 lines
1// dogma band: an optional Claude Code mod that shows the restricting
2// DOGMA-PERMISSIONS.md entries above the prompt (below credo's band, when
3// that is installed) and flashes the last dogma block. It only READS state
4// through scripts/permissions-summary.sh and renders it; dogma's hooks stay
5// the only enforcer and work the same without this band.
6
7import { atom, read, update } from 'claude-code'
8import type { EngineInterface, Register, RenderChildren } from 'claude-code'
9
10import type { DogmaBlock, DogmaGone, DogmaSummary } from '../types'
11
12// credo's band preset, read without depending on credo: dogma declares only
13// this one value of credo's state here (not in its own contract). Without
14// credo installed nothing ever writes it, the read answers the default 0
15// ('all') and the dogma band always shows.
16declare module 'claude-code' {
17  interface PluginState {
18    credo: { preset: number }
19  }
20}
21
22const HEAD = '◆ dogma'
23const GAP = 2
24const HEAD_GAP = 2
25
26const summary = atom({ plugin: 'dogma', key: 'summary' } as const, null)
27const moved = atom({ plugin: 'dogma', key: 'moved' } as const, [])
28const fresh = atom({ plugin: 'dogma', key: 'fresh' } as const, [])
29const gone = atom({ plugin: 'dogma', key: 'gone' } as const, [])
30const blink = atom({ plugin: 'dogma', key: 'blink' } as const, false)
31const block = atom({ plugin: 'dogma', key: 'block' } as const, null)
32// credo's preset stage 3 ("open only") hides the dogma band
33const credoPreset = atom({ plugin: 'credo', key: 'preset' } as const, 0)
34const HIDDEN_AT_STAGE = 3
35const PRESET_COUNT = 4
36
37const HIGHLIGHT = '#d946ef'
38const NEW_COLOR = 'whiteBright'
39const HIGHLIGHT_MS = 6000
40const BLINK_MS = 500
41const BLOCK_MS = 15000
42const BLOCK_BLINK_MS = 6000
43const REFRESH_MS = 10000
44const NOTICE_TOAST_MS = 12000
45// no dogma toast is shorter than this (the toast default is 4 s)
46const TOAST_MIN_MS = 6000
47
48type Piece = { width: number; node: RenderChildren; gap?: number }
49
50// greedy line packing: each piece stays whole, a line never exceeds columns
51function pack(pieces: Piece[], columns: number, gap: number): Piece[][] {
52  let line: Piece[] = []
53  const lines: Piece[][] = [line]
54  let used = 0
55  for (const p of pieces) {
56    const need = (line.length ? (p.gap ?? gap) : 0) + p.width
57    if (line.length && used + need > columns) {
58      line = [p]
59      lines.push(line)
60      used = p.width
61    } else {
62      line.push(p)
63      used += need
64    }
65  }
66  return lines
67}
68
69// summary of the session's cwd; exit 4 (no DOGMA-PERMISSIONS.md) or any failure hides the band
70async function refresh($: EngineInterface) {
71  // exit 4 (no DOGMA-PERMISSIONS.md) hides the band; any other failure (a timeout
72  // under load) keeps the last known value and logs the reason to the debug log
73  let next: DogmaSummary | null = null
74  try {
75    const r = await $.process.run([`${$.plugin.root}/scripts/permissions-summary.sh`, '--json'], { timeoutMs: 5000 })
76    if (r.exitCode !== 0 && r.exitCode !== 4) {
77      $.ui.log(`dogma band: permissions-summary.sh exit ${r.exitCode}: ${r.stderr.trim()}`, { to: 'debug' })
78      return
79    }
80    next = r.exitCode === 0 ? JSON.parse(r.stdout) : null
81  } catch (err) {
82    $.ui.log(`dogma band: permissions-summary.sh failed: ${String(err)}`, { to: 'debug' })
83    return
84  }
85  const prev = await read($, summary)
86  await update($, summary, () => next)
87  if (prev === null || next === null) return
88
89  // exact per-entry diff: kind switch = moved, newly restricted = fresh, dropped = gone
90  const kindOf = (s: DogmaSummary, label: string) =>
91    s.deny.includes(label) ? 'deny' : s.ask.includes(label) ? 'ask' : null
92  const before = [...prev.deny, ...prev.ask]
93  const after = [...next.deny, ...next.ask]
94  const movedNow = after.filter(l => before.includes(l) && kindOf(prev, l) !== kindOf(next, l))
95  const freshNow = after.filter(l => !before.includes(l))
96  const goneNow: DogmaGone[] = before
97    .filter(l => !after.includes(l))
98    .map(l => ({ kind: kindOf(prev, l) === 'deny' ? 'deny' : 'ask', label: l }))
99  if (!movedNow.length && !freshNow.length && !goneNow.length) return
100
101  await update($, moved, () => movedNow)
102  await update($, fresh, () => freshNow)
103  await update($, gone, () => goneNow)
104  for (let t = 0; t < HIGHLIGHT_MS; t += BLINK_MS) {
105    await update($, blink, v => !v)
106    await $.clock.sleep(BLINK_MS)
107  }
108  await update($, blink, () => false)
109  await update($, moved, () => [])
110  await update($, fresh, () => [])
111  await update($, gone, () => [])
112}
113
114// one toast at session start when update notices are pending for this repo, plugin
115// notices and dogma source broadcasts together; Claude asks about them (SessionStart
116// hook notices-inject.sh), this only hints.
117// exit 4 = none pending; any other failure only goes to the debug log
118async function noticeToast($: EngineInterface) {
119  try {
120    const r = await $.process.run([`${$.plugin.root}/scripts/notices-pending.sh`, '--json'], { timeoutMs: 5000 })
121    if (r.exitCode === 4) return
122    if (r.exitCode !== 0) {
123      $.ui.log(`dogma band: notices-pending.sh exit ${r.exitCode}: ${r.stderr.trim()}`, { to: 'debug' })
124      return
125    }
126    const n = (JSON.parse(r.stdout) as { notices?: unknown[] }).notices?.length ?? 0
127    if (n > 0) $.ui.toast(`dogma: ${n} update notice${n === 1 ? '' : 's'} - Claude will ask you`, { timeoutMs: NOTICE_TOAST_MS })
128  } catch (err) {
129    $.ui.log(`dogma band: notices-pending.sh failed: ${String(err)}`, { to: 'debug' })
130  }
131}
132
133// show the last dogma block for a few seconds, blinking, plus a toast
134async function showBlock($: EngineInterface, b: DogmaBlock) {
135  $.ui.toast(`dogma blocked ${b.tool}: ${b.reason}`, { timeoutMs: TOAST_MIN_MS })
136  // stamped, so the render hides it after BLOCK_MS even if this timer never finishes
137  // (plugin reload mid-sleep) and a stale stored block never sticks
138  const at = await $.clock.now()
139  await update($, block, () => ({ ...b, at }))
140  // blink for the first BLOCK_BLINK_MS, then stay steady red until BLOCK_MS
141  for (let t = 0; t < BLOCK_BLINK_MS; t += BLINK_MS) {
142    await update($, blink, v => !v)
143    await $.clock.sleep(BLINK_MS)
144  }
145  await update($, blink, () => false)
146  await $.clock.sleep(BLOCK_MS - BLOCK_BLINK_MS)
147  await update($, block, () => null)
148}
149
150export const register: Register = on => {
151  on('session.start', async ($, e, next) => {
152    const started = await next(e)
153    await update($, block, () => null)
154    await refresh($)
155    $.clock.every(REFRESH_MS, () => void refresh($))
156    void noticeToast($)
157    return started
158  })
159
160  // any tool may have edited DOGMA-PERMISSIONS.md
161  on('tool.call', async ($, e, next) => {
162    const result = await next(e)
163    void refresh($)
164    // a dogma PreToolUse deny reaches us as a refused or errored result whose
165    // text carries "BLOCKED" / "BLOCKED by dogma"; dogma decided, we only show it.
166    // A successful tool output that merely contains the word is ignored.
167    const text =
168      typeof result.deny === 'string' ? result.deny : result.isError === true && typeof result.text === 'string' ? result.text : ''
169    const m = text.match(/BLOCKED(?: by dogma)?:\s*(.+)/)
170    if (m?.[1]) void showBlock($, { tool: String(e.tool), reason: m[1].split(/(?<=\.)\s/)[0] ?? m[1] })
171    return result
172  }).catch(($, e, next) => next(e))
173
174  on('turn.complete', async ($, e, next) => {
175    void refresh($)
176    return next(e)
177  })
178
179  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
180    const below = await next(e)
181    const s = await read($, summary)
182    if (e.props.hasSurvey || s === null) return below
183    if ((await read($, credoPreset)) % PRESET_COUNT === HIDDEN_AT_STAGE) return below
184
185    const { Box, Text } = $.ui.resolve(e)
186    const restricted = s.ask.length + s.deny.length > 0
187    const isOn = await read($, blink)
188    const movedNow = isOn ? await read($, moved) : []
189    const freshNow = isOn ? await read($, fresh) : []
190    const goneNow = await read($, gone)
191    const tint = (label: string, color: string) =>
192      movedNow.includes(label) ? HIGHLIGHT : freshNow.includes(label) ? NEW_COLOR : color
193
194    const headColor = s.deny.length ? 'red' : restricted ? 'yellow' : 'green'
195    const pad = (text: string, width: number) => text + ' '.repeat(Math.max(0, width - text.length))
196
197    // one block per kind: the kind label in a fixed column, its entries packed
198    // beside it, continuation lines flush with the first entry
199    const KIND = 4
200    const room = e.props.bodyColumns - HEAD.length - HEAD_GAP - KIND - HEAD_GAP - 1
201    const blocks: [string, string[], string][] = [
202      ['deny', s.deny, 'red'],
203      ['ask', s.ask, 'yellow'],
204    ]
205    const rows: RenderChildren[] = []
206    for (const [kind, labels, color] of blocks) {
207      // removed entries stay visible, dim and struck through, for the highlight window
208      const dropped = goneNow.filter(g => g.kind === kind).map(g => g.label)
209      if (!labels.length && !dropped.length) continue
210      const pieces: Piece[] = [
211        ...labels.map(label => ({
212          width: label.length,
213          node: (
214            <Box key={`${kind}-${label}`}>
215              <Text bold color={tint(label, color)}>{label}</Text>
216            </Box>
217          ),
218        })),
219        ...dropped.map(label => ({
220          width: label.length,
221          node: (
222            <Box key={`${kind}-gone-${label}`}>
223              <Text dimColor strikethrough>{label}</Text>
224            </Box>
225          ),
226        })),
227      ]
228      pack(pieces, room, GAP).forEach((line, li) => {
229        rows.push(
230          <Box key={`${kind}${li}`} columnGap={HEAD_GAP}>
231            {rows.length === 0 ? <Text bold color={headColor}>{HEAD}</Text> : <Text>{pad('', HEAD.length)}</Text>}
232            <Text dimColor>{pad(li === 0 ? kind : '', KIND)}</Text>
233            <Box>{line.flatMap((p, i) => (i ? [<Text key={`gap${i}`}>{' '.repeat(p.gap ?? GAP)}</Text>, p.node] : [p.node]))}</Box>
234          </Box>,
235        )
236      })
237    }
238    if (!rows.length) {
239      rows.push(
240        <Box key="auto" columnGap={HEAD_GAP}>
241          <Text bold color={headColor}>{HEAD}</Text>
242          <Text dimColor>all auto</Text>
243        </Box>,
244      )
245    }
246    const last = await read($, block)
247    if (last && typeof last.at === 'number' && (await $.clock.now()) - last.at < BLOCK_MS) {
248      rows.push(
249        <Box key="block" columnGap={HEAD_GAP}>
250          <Text bold color={isOn ? HIGHLIGHT : 'red'}>⛔ blocked</Text>
251          <Text>
252            <Text bold>{last.tool}</Text>
253            <Text dimColor>: {last.reason}</Text>
254          </Text>
255        </Box>,
256      )
257    }
258    const mine = <Box flexDirection="column">{rows}</Box>
259
260    // dogma below another band (credo)
261    return (
262      <Box flexDirection="column">
263        {below}
264        {mine}
265      </Box>
266    )
267  })
268}
269
types/index.d.ts 26 lines
1// State contract of dogma's optional Claude Code band (hooks/band.tsx).
2// Every value is a read-only mirror of what scripts/permissions-summary.sh
3// prints, plus short-lived highlights; dogma's hooks stay the only enforcer.
4
5/** scripts/permissions-summary.sh --json */
6export type DogmaSummary = { file: string; ask: string[]; deny: string[] }
7
8/** an entry that left the summary, shown struck through for a few seconds */
9export type DogmaGone = { kind: 'deny' | 'ask'; label: string }
10
11/** the last dogma PreToolUse block, shown for a few seconds */
12export type DogmaBlock = { tool: string; reason: string; at?: number }
13
14declare module 'claude-code' {
15  interface PluginState {
16    dogma: {
17      summary: DogmaSummary | null
18      moved: string[]
19      fresh: string[]
20      gone: DogmaGone[]
21      blink: boolean
22      block: DogmaBlock | null
23    }
24  }
25}
26