From cc9761df47970f442d5e4033aeb1ee20c6cdc078 Mon Sep 17 00:00:00 2001 From: fitz123 Date: Mon, 18 May 2026 00:24:32 +0300 Subject: [PATCH 1/5] platform: promote 10 generic rules from custom/ for family-agent inheritance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promotes these rules from private-workspace custom/ to upstream platform/ so all consumers (main agent + family agents Anna/Yulia/Coder/Cyber-Architect) inherit them via .claude/rules/platform/ directory symlink: As-is: - anti-amnesia, bot-code-readonly, contradictions, every-session, ralphex-notify-thread, show-evidence, sops-no-decrypt-stdout With edits (depersonalization + carve-outs for non-code agents): - implementation-protocol: "Main agent" -> "You"; carve-outs in both "Separation of Concerns" and "Ralphex - When to Use" sections so non-code agents skip cleanly - public-repo-pii: drop hardcoded fitz123/claude-code-bot reference - runtime-context: parameterize workspace paths, drop main-only agent roster (agents discover their roster via config.yaml) Stays in private workspace custom/: - local-tools, reference-structure, release-flow, reminders, safety-workspace, task-tracking, fix-verification, heartbeats (workspace-specific tooling, repo paths, or Ninja-only access) Dual-review iterations: - Iter 1: family-angle reviewer found 2 Critical (Ninja-hardcoded in show-evidence + contradictions) + 3 High; main-angle clean. Addressed. - Iter 2: family-angle reviewer found 2 High (implementation-protocol needed second carve-out; every-session needs decisions.md in family workspaces). One fixed in rule; the other deferred to companion workspace PR that symlinks decisions.md into family workspaces and documents the symlink set as a runbook. - Iter 3: family-angle reviewer confirms clean. Follow-up (separate PR): pre-existing platform rules with hardcoded "Ninja" (delegation, memory-protocol, communication, bot-operations) — out of scope here. Co-Authored-By: Claude Opus 4.7 --- .claude/rules/platform/anti-amnesia.md | 5 +++ .claude/rules/platform/bot-code-readonly.md | 13 ++++++ .claude/rules/platform/contradictions.md | 9 ++++ .claude/rules/platform/every-session.md | 8 ++++ .../rules/platform/implementation-protocol.md | 41 +++++++++++++++++++ .claude/rules/platform/public-repo-pii.md | 21 ++++++++++ .../rules/platform/ralphex-notify-thread.md | 24 +++++++++++ .claude/rules/platform/runtime-context.md | 34 +++++++++++++++ .claude/rules/platform/show-evidence.md | 17 ++++++++ .../rules/platform/sops-no-decrypt-stdout.md | 40 ++++++++++++++++++ 10 files changed, 212 insertions(+) create mode 100644 .claude/rules/platform/anti-amnesia.md create mode 100644 .claude/rules/platform/bot-code-readonly.md create mode 100644 .claude/rules/platform/contradictions.md create mode 100644 .claude/rules/platform/every-session.md create mode 100644 .claude/rules/platform/implementation-protocol.md create mode 100644 .claude/rules/platform/public-repo-pii.md create mode 100644 .claude/rules/platform/ralphex-notify-thread.md create mode 100644 .claude/rules/platform/runtime-context.md create mode 100644 .claude/rules/platform/show-evidence.md create mode 100644 .claude/rules/platform/sops-no-decrypt-stdout.md diff --git a/.claude/rules/platform/anti-amnesia.md b/.claude/rules/platform/anti-amnesia.md new file mode 100644 index 0000000..e201ce4 --- /dev/null +++ b/.claude/rules/platform/anti-amnesia.md @@ -0,0 +1,5 @@ +# Anti-Amnesia + +Before claiming "we did this" or "this was decided" — verify it exists. If you can't find a source, say "I don't remember this, let me check" instead of guessing. Wrong confidence is worse than honest amnesia. + +If you want to remember something — WRITE IT TO A FILE. "Mental notes" don't survive sessions. diff --git a/.claude/rules/platform/bot-code-readonly.md b/.claude/rules/platform/bot-code-readonly.md new file mode 100644 index 0000000..29b30bd --- /dev/null +++ b/.claude/rules/platform/bot-code-readonly.md @@ -0,0 +1,13 @@ +--- +paths: + - bot/** + - .claude/hooks/** + - .claude/rules/platform/** + - .claude/skills/workspace-health/scripts/** +--- + +# Bot & Platform Files — Read-Only in Workspace + +These files come from upstream (`fitz123/claude-code-bot`). Never edit them here. + +To change: PR in public repo (`~/src/claude-code-bot/`) → merge → `git fetch upstream && git merge upstream/main` in workspace. diff --git a/.claude/rules/platform/contradictions.md b/.claude/rules/platform/contradictions.md new file mode 100644 index 0000000..eab02e2 --- /dev/null +++ b/.claude/rules/platform/contradictions.md @@ -0,0 +1,9 @@ +# Contradictions Must Be Flagged + +When a request contradicts an existing rule, decision, evidence gate, ADR, or prior agreement — **STOP and flag it to the user before acting.** + +Do not silently resolve contradictions by ignoring one side. Explain: "This contradicts X because Y. How do you want to handle it?" + +The user may forget or miss contradictions too — that's expected. You are the filter. This applies everywhere: plans, tasks, implementation, cron setup, any decision point. + +We don't work with contradictions without an explicit reason. diff --git a/.claude/rules/platform/every-session.md b/.claude/rules/platform/every-session.md new file mode 100644 index 0000000..37f9f2a --- /dev/null +++ b/.claude/rules/platform/every-session.md @@ -0,0 +1,8 @@ +# Every Session + +Before doing anything else: + +1. Read `USER.md` — this is who you're helping +2. Read `reference/governance/decisions.md` — the Decision Log. Do NOT contradict ACTIVE decisions. + +Don't ask permission. Just do it. diff --git a/.claude/rules/platform/implementation-protocol.md b/.claude/rules/platform/implementation-protocol.md new file mode 100644 index 0000000..dfb17dd --- /dev/null +++ b/.claude/rules/platform/implementation-protocol.md @@ -0,0 +1,41 @@ +# Implementation Protocol + +## Separation of Concerns + +**You research and document. Ralphex implements.** + +This applies to everything code-related: bug fixes, features, new skills, scripts. Non-code agents (content, communication, planning) skip this rule when no code is touched. + +### Your role (when code is involved): +1. **Research** — understand the problem/need, read code, gather context +2. **Evidence** — logs, source audit, verify claims with sub-agents +3. **Document** — write confirmed problem/spec to GitHub issue (or workspace task tracker) with full evidence +4. **Plan** — ralphex-plan or plan skill as needed + +### Ralphex role: +5. **Solution** — finds the best approach (not first-obvious) +6. **Implementation** — multi-agent implementation pipeline +7. **Review** — `ralphex --review` on the branch + +## Ralphex — When to Use + +If you don't touch code (non-code agents), skip this section. + +Use `ralphex-plan` skill + `ralphex` CLI when ANY of these apply: +- Change is **dangerous** (data loss risk, security-sensitive, production infra) +- Change is **nontrivial** (touches >3 files, requires architectural decisions) +- You are **not 95%+ confident** you can implement it correctly on the first try +- Change modifies **core functionality** (not just config, docs, or one-liner fixes) + +Flow: `/ralphex-plan` → write plan → `ralphex ` → review result → merge. + +### Plan file location +Plans MUST be written to `docs/plans/` inside the target repo (ralphex default). After writing the plan, ensure the plan file is NOT staged/committed on main — ralphex creates its own worktree and copies the plan there. If auto-stage hook picks up the file, `git reset HEAD ` before launching ralphex. +Ralphex runs multi-agent implementation + review pipeline with rollback safety. Use it — don't hero-code risky changes. + +### Never skip to coding +Even if the change seems trivial — if it touches code, send to ralphex. +Exception: 1-line config changes by direct user instruction. + +### Bot bugs +When discovering bugs in `bot/src/` — create a GitHub issue with evidence and root cause analysis. Don't fix code directly. Document in issue, let ralphex implement. diff --git a/.claude/rules/platform/public-repo-pii.md b/.claude/rules/platform/public-repo-pii.md new file mode 100644 index 0000000..3156db4 --- /dev/null +++ b/.claude/rules/platform/public-repo-pii.md @@ -0,0 +1,21 @@ +# No PII in Public Repositories + +**NEVER** include any of the following in public repos (issues, PRs, comments, code, commits): + +- Real names, usernames, Telegram handles +- Chat IDs, user IDs, group IDs +- Addresses, phone numbers, emails +- Bot tokens, API keys, credentials (even in stack traces) +- Group/channel names that identify the owner +- Any data that links back to a real person + +### When posting logs or stack traces: +- Replace chat IDs with `` +- Replace bot tokens with `` +- Replace usernames with generic terms ("the user", "the admin") +- Replace group names with generic terms ("the group chat") + +### Before every `gh issue`, `gh pr`, or `gh api` write: +Ask yourself: "Does this contain anything that identifies a real person?" + +This applies to any public repository. diff --git a/.claude/rules/platform/ralphex-notify-thread.md b/.claude/rules/platform/ralphex-notify-thread.md new file mode 100644 index 0000000..651f9eb --- /dev/null +++ b/.claude/rules/platform/ralphex-notify-thread.md @@ -0,0 +1,24 @@ +# Ralphex Notifications — Current Topic + +If you don't launch `ralphex` (non-code agents), skip this rule. + +When launching `ralphex` from a Telegram session that isn't the default Ops topic, set `RALPHEX_NOTIFY_THREAD=` so completion/error notifications come back to the same topic where the user requested the run. + +The thread ID is in the chat header of every incoming message: `[Chat: | Topic: | From: ...]`. + +## How to apply + +Include the env var in the `nohup ralphex` command: + +```bash +cd && \ +CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \ +RALPHEX_NOTIFY_THREAD= \ +nohup ralphex --debug --no-color > 2>&1 & +``` + +The `notify-minime.sh` script honors `RALPHEX_NOTIFY_THREAD` over its PWD heuristic (which only catches `*/.minime/bot*`). + +## Why + +User asks for the run from a specific topic — they want results delivered there, not in the default Ops feed. Otherwise the user has to switch topics to see whether their run completed. diff --git a/.claude/rules/platform/runtime-context.md b/.claude/rules/platform/runtime-context.md new file mode 100644 index 0000000..e318a0d --- /dev/null +++ b/.claude/rules/platform/runtime-context.md @@ -0,0 +1,34 @@ +# Runtime Context + +You are running as a **Claude Code CLI subprocess**, spawned by the grammY Telegram bot. + +## How you were started + +- The Telegram bot spawned your process via `claude -p` with stream-json protocol +- Messages arrive from Telegram, routed through bot bindings to your session +- Each Telegram chat gets its own Claude Code subprocess with separate conversation context +- The bot runs under a Max subscription (no API keys, fixed monthly cost) — applies to all agents spawned by this bot + +## What this means + +- You are a Claude Code CLI process. You have full Claude Code capabilities (Read, Edit, Write, Bash, Agent, etc.) +- You are NOT running in a terminal. Messages come from Telegram users, not a keyboard +- Your responses are sent back to Telegram via the bot's stream relay +- Your workspace is your current working directory. Other agents live in sibling directories alongside it; check the bot's `config.yaml` (in the main workspace) for the full agent roster and which Telegram chats route to which agent +- Bot tools are available: `bot/scripts/deliver.sh` for Telegram messaging, `launchctl` for cron management + +## Session transcripts + +Claude Code CLI stores session transcripts as JSONL files. Path pattern: +``` +~/.claude/projects/-/*.jsonl +``` +The workspace path is dash-encoded (every `/` becomes `-`). To find your own transcripts: `pwd | sed 's:/:-:g'` then look under `~/.claude/projects/`. + +Use these to search conversation history, find when/where decisions were made, trace WebFetch URLs, etc. + +## Delegation + +- Use Claude Code's native `Agent` tool for sub-tasks (NOT `sessions_spawn`) +- For cron/scheduled tasks: launchd plists in `~/Library/LaunchAgents/` +- For one-off delayed tasks: `at` command or launchd one-shot plist diff --git a/.claude/rules/platform/show-evidence.md b/.claude/rules/platform/show-evidence.md new file mode 100644 index 0000000..be5368f --- /dev/null +++ b/.claude/rules/platform/show-evidence.md @@ -0,0 +1,17 @@ +# Show Evidence + +When asserting a fact to the user — show the proof inline. No naked claims. + +| Claim type | Evidence | +|---|---| +| Process running | `tail` of its log or `ps` output | +| Code does X | Relevant lines from file | +| Config set to Y | Snippet from config file | +| Error happened | Log lines or error output | +| File exists/changed | `ls -la` or diff | + +Rule: **assertion without evidence = lie.** If you can't show proof, say "let me check" instead of stating it as fact. + +## PR Self-Check + +After creating or updating a PR, always run `gh pr checks ` and show the result. Don't wait for the user to ask — check it yourself, fix if red. diff --git a/.claude/rules/platform/sops-no-decrypt-stdout.md b/.claude/rules/platform/sops-no-decrypt-stdout.md new file mode 100644 index 0000000..0347f81 --- /dev/null +++ b/.claude/rules/platform/sops-no-decrypt-stdout.md @@ -0,0 +1,40 @@ +# SOPS: Never decrypt to stdout in agent transcripts + +`sops -d ` outputs plaintext secrets to stdout. In a Claude Code Bash tool +call, that stdout lands in the session JSONL transcript and persists on disk. +**Plaintext secrets in transcripts = compromise.** Real incident on file: +a sub-agent ran `sops -d ` without filtering and leaked +API tokens into the agent transcript. + +## Rule + +When verifying sops-encrypted files in Bash: + +- **ALLOWED:** pipe to filter that strips values + - `sops -d | cut -d= -f1` (keys only) + - `sops -d | wc -l` (count only) + - `sops -d | grep -c '=' ` (count of entries) + - `sops -d >/dev/null && echo ok` (round-trip check) +- **FORBIDDEN:** raw decrypt to terminal + - `sops -d ` (alone) + - `sops -d | head` + - `sops -d 2>&1` (without filter) + +## When delegating review to sub-agents + +Review prompts that ask sub-agents to inspect sops files **MUST include**: + +> Do NOT run `sops -d` without piping to a value-stripping filter (`cut -d= -f1`, +> `wc -l`, or `>/dev/null`). Plaintext to stdout leaks into transcripts. + +## Editing secrets + +Use `sops ` (interactive, no stdout) — it spawns `$EDITOR` on a temp +plaintext file, re-encrypts on save. Never `sops -d > tmp; edit tmp; sops -e tmp`. + +## Why this matters + +Transcripts under `~/.claude/projects/-*/` are not gitignored from system +backups (Time Machine, rsync), they're readable by any process running as +the user, and they're a long-lived record. One careless `sops -d` = persistent +disclosure. From 4956461fcef0c39f859128c7648261441aa95d17 Mon Sep 17 00:00:00 2001 From: fitz123 Date: Mon, 18 May 2026 00:42:00 +0300 Subject: [PATCH 2/5] fix: address review comments on PR #125 Owner left 7 inline comments. Resolutions: - every-session.md: deleted (retired entirely). Added @reference/governance/decisions.md to CLAUDE.md so the directive becomes an auto-import instead of a procedural rule. USER.md, IDENTITY.md, MEMORY.md were already imported. - implementation-protocol.md: deleted (main-only - non-code agents do not use ralphex). - public-repo-pii.md: deleted (main-only - only main pushes to public repos). - ralphex-notify-thread.md: deleted (main-only - ralphex-specific). - show-evidence.md: deleted (main-only). - bot-code-readonly.md: expanded paths frontmatter from 4 to 12 entries (.github/workflows/**, .githooks/**, .gitleaks.toml, .gitleaksignore, CLAUDE.md, README.md, config.yaml, config.local.yaml.example). Added paragraph noting protect-files.sh hook only enforces .claude/skills/** for crons - interactive sessions are self-discipline only. Final scope: 5 promoted rules + CLAUDE.md import. Co-Authored-By: Claude Opus 4.7 --- .claude/rules/platform/bot-code-readonly.md | 12 ++++++ .claude/rules/platform/every-session.md | 8 ---- .../rules/platform/implementation-protocol.md | 41 ------------------- .claude/rules/platform/public-repo-pii.md | 21 ---------- .../rules/platform/ralphex-notify-thread.md | 24 ----------- .claude/rules/platform/show-evidence.md | 17 -------- CLAUDE.md | 1 + 7 files changed, 13 insertions(+), 111 deletions(-) delete mode 100644 .claude/rules/platform/every-session.md delete mode 100644 .claude/rules/platform/implementation-protocol.md delete mode 100644 .claude/rules/platform/public-repo-pii.md delete mode 100644 .claude/rules/platform/ralphex-notify-thread.md delete mode 100644 .claude/rules/platform/show-evidence.md diff --git a/.claude/rules/platform/bot-code-readonly.md b/.claude/rules/platform/bot-code-readonly.md index 29b30bd..d2460f2 100644 --- a/.claude/rules/platform/bot-code-readonly.md +++ b/.claude/rules/platform/bot-code-readonly.md @@ -4,6 +4,14 @@ paths: - .claude/hooks/** - .claude/rules/platform/** - .claude/skills/workspace-health/scripts/** + - .github/workflows/** + - .githooks/** + - .gitleaks.toml + - .gitleaksignore + - CLAUDE.md + - README.md + - config.yaml + - config.local.yaml.example --- # Bot & Platform Files — Read-Only in Workspace @@ -11,3 +19,7 @@ paths: These files come from upstream (`fitz123/claude-code-bot`). Never edit them here. To change: PR in public repo (`~/src/claude-code-bot/`) → merge → `git fetch upstream && git merge upstream/main` in workspace. + +The `paths:` list above is the canonical set of upstream-owned paths. Any local edit to these breaks the next `git merge upstream/main` (divergence/conflicts) and risks losing your change. The `protect-files.sh` hook only blocks `.claude/skills/**` writes from crons — it does **not** enforce this rule for interactive sessions. Self-discipline carries the load here. + +Workspace-only files (USER.md, IDENTITY.md, MEMORY.md, memory/, .claude/rules/custom/, .claude/skills/ outside upstream-tracked names, reference/, docs/, scripts/) are safe to edit locally. diff --git a/.claude/rules/platform/every-session.md b/.claude/rules/platform/every-session.md deleted file mode 100644 index 37f9f2a..0000000 --- a/.claude/rules/platform/every-session.md +++ /dev/null @@ -1,8 +0,0 @@ -# Every Session - -Before doing anything else: - -1. Read `USER.md` — this is who you're helping -2. Read `reference/governance/decisions.md` — the Decision Log. Do NOT contradict ACTIVE decisions. - -Don't ask permission. Just do it. diff --git a/.claude/rules/platform/implementation-protocol.md b/.claude/rules/platform/implementation-protocol.md deleted file mode 100644 index dfb17dd..0000000 --- a/.claude/rules/platform/implementation-protocol.md +++ /dev/null @@ -1,41 +0,0 @@ -# Implementation Protocol - -## Separation of Concerns - -**You research and document. Ralphex implements.** - -This applies to everything code-related: bug fixes, features, new skills, scripts. Non-code agents (content, communication, planning) skip this rule when no code is touched. - -### Your role (when code is involved): -1. **Research** — understand the problem/need, read code, gather context -2. **Evidence** — logs, source audit, verify claims with sub-agents -3. **Document** — write confirmed problem/spec to GitHub issue (or workspace task tracker) with full evidence -4. **Plan** — ralphex-plan or plan skill as needed - -### Ralphex role: -5. **Solution** — finds the best approach (not first-obvious) -6. **Implementation** — multi-agent implementation pipeline -7. **Review** — `ralphex --review` on the branch - -## Ralphex — When to Use - -If you don't touch code (non-code agents), skip this section. - -Use `ralphex-plan` skill + `ralphex` CLI when ANY of these apply: -- Change is **dangerous** (data loss risk, security-sensitive, production infra) -- Change is **nontrivial** (touches >3 files, requires architectural decisions) -- You are **not 95%+ confident** you can implement it correctly on the first try -- Change modifies **core functionality** (not just config, docs, or one-liner fixes) - -Flow: `/ralphex-plan` → write plan → `ralphex ` → review result → merge. - -### Plan file location -Plans MUST be written to `docs/plans/` inside the target repo (ralphex default). After writing the plan, ensure the plan file is NOT staged/committed on main — ralphex creates its own worktree and copies the plan there. If auto-stage hook picks up the file, `git reset HEAD ` before launching ralphex. -Ralphex runs multi-agent implementation + review pipeline with rollback safety. Use it — don't hero-code risky changes. - -### Never skip to coding -Even if the change seems trivial — if it touches code, send to ralphex. -Exception: 1-line config changes by direct user instruction. - -### Bot bugs -When discovering bugs in `bot/src/` — create a GitHub issue with evidence and root cause analysis. Don't fix code directly. Document in issue, let ralphex implement. diff --git a/.claude/rules/platform/public-repo-pii.md b/.claude/rules/platform/public-repo-pii.md deleted file mode 100644 index 3156db4..0000000 --- a/.claude/rules/platform/public-repo-pii.md +++ /dev/null @@ -1,21 +0,0 @@ -# No PII in Public Repositories - -**NEVER** include any of the following in public repos (issues, PRs, comments, code, commits): - -- Real names, usernames, Telegram handles -- Chat IDs, user IDs, group IDs -- Addresses, phone numbers, emails -- Bot tokens, API keys, credentials (even in stack traces) -- Group/channel names that identify the owner -- Any data that links back to a real person - -### When posting logs or stack traces: -- Replace chat IDs with `` -- Replace bot tokens with `` -- Replace usernames with generic terms ("the user", "the admin") -- Replace group names with generic terms ("the group chat") - -### Before every `gh issue`, `gh pr`, or `gh api` write: -Ask yourself: "Does this contain anything that identifies a real person?" - -This applies to any public repository. diff --git a/.claude/rules/platform/ralphex-notify-thread.md b/.claude/rules/platform/ralphex-notify-thread.md deleted file mode 100644 index 651f9eb..0000000 --- a/.claude/rules/platform/ralphex-notify-thread.md +++ /dev/null @@ -1,24 +0,0 @@ -# Ralphex Notifications — Current Topic - -If you don't launch `ralphex` (non-code agents), skip this rule. - -When launching `ralphex` from a Telegram session that isn't the default Ops topic, set `RALPHEX_NOTIFY_THREAD=` so completion/error notifications come back to the same topic where the user requested the run. - -The thread ID is in the chat header of every incoming message: `[Chat: | Topic: | From: ...]`. - -## How to apply - -Include the env var in the `nohup ralphex` command: - -```bash -cd && \ -CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \ -RALPHEX_NOTIFY_THREAD= \ -nohup ralphex --debug --no-color > 2>&1 & -``` - -The `notify-minime.sh` script honors `RALPHEX_NOTIFY_THREAD` over its PWD heuristic (which only catches `*/.minime/bot*`). - -## Why - -User asks for the run from a specific topic — they want results delivered there, not in the default Ops feed. Otherwise the user has to switch topics to see whether their run completed. diff --git a/.claude/rules/platform/show-evidence.md b/.claude/rules/platform/show-evidence.md deleted file mode 100644 index be5368f..0000000 --- a/.claude/rules/platform/show-evidence.md +++ /dev/null @@ -1,17 +0,0 @@ -# Show Evidence - -When asserting a fact to the user — show the proof inline. No naked claims. - -| Claim type | Evidence | -|---|---| -| Process running | `tail` of its log or `ps` output | -| Code does X | Relevant lines from file | -| Config set to Y | Snippet from config file | -| Error happened | Log lines or error output | -| File exists/changed | `ls -la` or diff | - -Rule: **assertion without evidence = lie.** If you can't show proof, say "let me check" instead of stating it as fact. - -## PR Self-Check - -After creating or updating a PR, always run `gh pr checks ` and show the result. Don't wait for the user to ask — check it yourself, fix if red. diff --git a/CLAUDE.md b/CLAUDE.md index 6168606..0a03d00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,6 +3,7 @@ @USER.md @IDENTITY.md @MEMORY.md +@reference/governance/decisions.md ## Critical Rules From cdf41051dd835d481fc22cf5b38e6d042717b0ac Mon Sep 17 00:00:00 2001 From: fitz123 Date: Mon, 18 May 2026 01:12:19 +0300 Subject: [PATCH 3/5] hook: extend protect-files.sh to enforce bot-code-readonly paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per owner request — the bot-code-readonly rule was soft self-discipline. Hook now hard-blocks writes to upstream-owned platform files from all sessions (interactive + cron), mirroring the rule paths list. Bypass triggers (all log to stderr for transcript visibility): 1. PROTECT_FILES_BYPASS=1 — explicit opt-out 2. CLAUDE_PROJECT_DIR contains /.ralphex/worktrees/ — ralphex pipeline 3. git remote.origin.url at CLAUDE_PROJECT_DIR matches *fitz123/claude-code-bot(.git)?(/)?$ — upstream dev checkout CLAUDE.md and config.yaml removed from the rule paths list (workspace edits these locally via merge=ours / per-workspace bindings). Dual-review iterations: - Iter 1: 2 Critical found (ralphex worktrees blocked, upstream dev blocked). Addressed via bypass logic. - Iter 2: 2 Critical in bypass (basename spoofable, PWD fallthrough) + 1 High (silent env bypass). All addressed: git-remote check instead of basename, fail-closed on missing CLAUDE_PROJECT_DIR, stderr logging. - Iter 3: clean. Test matrix (7 cases, all pass): T1 USER.md → allowed T2 workspace bot/src → BLOCKED T3 upstream dev (real remote) → BYPASS + log T4 spoofed /tmp/claude-code-bot (no remote) → BLOCKED T5 CLAUDE_PROJECT_DIR unset → BLOCKED (fail-closed) T6 ralphex worktree → BYPASS + log T7 PROTECT_FILES_BYPASS=1 → BYPASS + log Co-Authored-By: Claude Opus 4.7 --- .claude/hooks/protect-files.sh | 64 ++++++++++++++++++++- .claude/rules/platform/bot-code-readonly.md | 10 ++-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/.claude/hooks/protect-files.sh b/.claude/hooks/protect-files.sh index 1a62ba9..870a8cc 100755 --- a/.claude/hooks/protect-files.sh +++ b/.claude/hooks/protect-files.sh @@ -1,6 +1,8 @@ #!/bin/bash # protect-files.sh — PreToolUse hook -# Blocks writes to protected skill files (for crons/autonomous agents only). +# Blocks writes to: +# 1) Skill files — cron/autonomous sessions only (interactive can still edit) +# 2) Upstream-owned platform files — ALL sessions (bot-code-readonly enforcement) # Fail-closed: if jq is missing, block rather than bypass if ! command -v jq &>/dev/null; then @@ -38,7 +40,7 @@ while [[ "$FILE_PATH" == *"/.."* ]]; do [[ "$FILE_PATH" == "$_prev" ]] && break done -# Protected: skill files (read-only for crons/autonomous agents) +# --- 1. Skills — cron-only block (interactive sessions can still edit) --- # Match both absolute (*/…) and relative (.claude/skills/…) paths if [[ "$FILE_PATH" == */.claude/skills/* ]] || [[ "$FILE_PATH" == .claude/skills/* ]]; then if [ -n "$CRON_NAME" ]; then @@ -47,4 +49,62 @@ if [[ "$FILE_PATH" == */.claude/skills/* ]] || [[ "$FILE_PATH" == .claude/skills fi fi +# --- 2. Upstream-owned platform files — block ALL sessions --- +# Mirror of `bot-code-readonly.md` paths frontmatter. Keep these two lists +# in lockstep — the rule is the doc, the hook is the enforcement. If you +# legitimately need to change one of these files: do it in upstream +# (fitz123/claude-code-bot) → PR → merge → `git fetch upstream && git merge`. + +# Bypass paths where editing these files IS the intended workflow. +# Three triggers — all log to stderr so bypass is visible in transcripts: +# 1. PROTECT_FILES_BYPASS=1 — explicit opt-out for one-off cases +# 2. $CLAUDE_PROJECT_DIR contains `/.ralphex/worktrees/` — ralphex pipeline +# 3. git remote.origin.url at $CLAUDE_PROJECT_DIR is the upstream repo +# +# Fail-closed on $CLAUDE_PROJECT_DIR — if unset, no bypass (no $PWD fallback, +# since $PWD can be an agent-controlled location whereas CLAUDE_PROJECT_DIR +# is set by the Claude Code harness from the session's project root). +PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-}" +bypass="" + +if [ "${PROTECT_FILES_BYPASS:-0}" = "1" ]; then + bypass="env PROTECT_FILES_BYPASS=1" +elif [ -n "$PROJECT_ROOT" ]; then + if [[ "$PROJECT_ROOT" == */.ralphex/worktrees/* ]]; then + bypass="ralphex worktree ($PROJECT_ROOT)" + else + origin_url="$(git -C "$PROJECT_ROOT" remote get-url origin 2>/dev/null || true)" + case "$origin_url" in + *fitz123/claude-code-bot.git|*fitz123/claude-code-bot|*fitz123/claude-code-bot/) + bypass="upstream dev repo (origin=$origin_url)" + ;; + esac + fi +fi + +if [ -n "$bypass" ]; then + echo "protect-files: bypass active — $bypass" >&2 + exit 0 +fi + +case "$FILE_PATH" in + */bot/*|bot/*) match=1 ;; + */.claude/hooks/*|.claude/hooks/*) match=1 ;; + */.claude/rules/platform/*|.claude/rules/platform/*) match=1 ;; + */.claude/skills/workspace-health/scripts/*|.claude/skills/workspace-health/scripts/*) match=1 ;; + */.github/workflows/*|.github/workflows/*) match=1 ;; + */.githooks/*|.githooks/*) match=1 ;; + */.gitleaks.toml|.gitleaks.toml) match=1 ;; + */.gitleaksignore|.gitleaksignore) match=1 ;; + */README.md|README.md) match=1 ;; + */config.local.yaml.example|config.local.yaml.example) match=1 ;; + *) match=0 ;; +esac + +if [ "$match" = "1" ]; then + echo "BLOCKED by protect-files: '$FILE_PATH' is upstream-owned (see .claude/rules/platform/bot-code-readonly.md)." >&2 + echo "Change it in fitz123/claude-code-bot via PR, then 'git fetch upstream && git merge upstream/main'." >&2 + exit 2 +fi + exit 0 diff --git a/.claude/rules/platform/bot-code-readonly.md b/.claude/rules/platform/bot-code-readonly.md index d2460f2..7db1170 100644 --- a/.claude/rules/platform/bot-code-readonly.md +++ b/.claude/rules/platform/bot-code-readonly.md @@ -8,9 +8,7 @@ paths: - .githooks/** - .gitleaks.toml - .gitleaksignore - - CLAUDE.md - README.md - - config.yaml - config.local.yaml.example --- @@ -20,6 +18,10 @@ These files come from upstream (`fitz123/claude-code-bot`). Never edit them here To change: PR in public repo (`~/src/claude-code-bot/`) → merge → `git fetch upstream && git merge upstream/main` in workspace. -The `paths:` list above is the canonical set of upstream-owned paths. Any local edit to these breaks the next `git merge upstream/main` (divergence/conflicts) and risks losing your change. The `protect-files.sh` hook only blocks `.claude/skills/**` writes from crons — it does **not** enforce this rule for interactive sessions. Self-discipline carries the load here. +The `paths:` list above is the canonical set of upstream-owned paths. Any local edit to these breaks the next `git merge upstream/main` (divergence/conflicts) and risks losing your change. The `protect-files.sh` hook enforces this list for **all** sessions — `Edit`/`Write` on a matching path fails fast with a pointer back to this rule. -Workspace-only files (USER.md, IDENTITY.md, MEMORY.md, memory/, .claude/rules/custom/, .claude/skills/ outside upstream-tracked names, reference/, docs/, scripts/) are safe to edit locally. +Files that **look upstream but are workspace-local** (excluded from the list above) and ARE safe to edit: + +- `CLAUDE.md` — `.gitattributes merge=ours` keeps your local version on merge. +- `config.yaml`, `config.local.yaml` — each workspace has its own bindings/agents. +- `USER.md`, `IDENTITY.md`, `MEMORY.md`, `memory/`, `.claude/rules/custom/`, `.claude/skills/` (outside upstream-tracked names), `reference/`, `docs/`, `scripts/`. From c03fe601fafe0eeb607e3382514a945a6ffe77a2 Mon Sep 17 00:00:00 2001 From: fitz123 Date: Mon, 18 May 2026 01:31:32 +0300 Subject: [PATCH 4/5] hook: anchor protect-files patterns to repo root (Copilot review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot flagged that case patterns like */bot/*|bot/* match the `bot/` segment at ANY depth — false-blocking nested paths like `reference/bot/notes.md` or `docs/bot/x.md`. The frontmatter list in bot-code-readonly.md is rooted (bot/**, .github/workflows/**, etc) so the hook must match the same way. Compute REL_PATH = FILE_PATH minus $CLAUDE_PROJECT_DIR prefix once, then match against rooted patterns (`bot/*`, `.github/workflows/*`, ...). Same anchoring applied to the existing skills block. Tests (8 cases, all pass): T1 USER.md → allowed T2 /workspace/bot/src/foo.ts → BLOCKED T3 /workspace/reference/bot/notes.md → ALLOWED (was wrongly blocked) T4 /workspace/docs/bot/x.md → ALLOWED (was wrongly blocked) T5 /workspace/reference/.github/workflows/x.yml → ALLOWED T6 .github/workflows/x.yml at root → BLOCKED T7 CLAUDE.md → allowed T8 upstream dev repo bypass → BYPASS + log Co-Authored-By: Claude Opus 4.7 --- .claude/hooks/protect-files.sh | 61 +++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/.claude/hooks/protect-files.sh b/.claude/hooks/protect-files.sh index 870a8cc..85f317d 100755 --- a/.claude/hooks/protect-files.sh +++ b/.claude/hooks/protect-files.sh @@ -40,15 +40,35 @@ while [[ "$FILE_PATH" == *"/.."* ]]; do [[ "$FILE_PATH" == "$_prev" ]] && break done -# --- 1. Skills — cron-only block (interactive sessions can still edit) --- -# Match both absolute (*/…) and relative (.claude/skills/…) paths -if [[ "$FILE_PATH" == */.claude/skills/* ]] || [[ "$FILE_PATH" == .claude/skills/* ]]; then - if [ -n "$CRON_NAME" ]; then - echo "Blocked: cron '$CRON_NAME' cannot modify skill files: $FILE_PATH" >&2 - exit 2 - fi +# Compute repo-rooted relative path so subsequent globs anchor to the +# repository root, not to an arbitrary path segment. Without this, a glob +# like `*/bot/*` would also match `reference/bot/notes.md` (the literal +# `bot` segment can occur anywhere in the tree). The frontmatter in +# bot-code-readonly.md is rooted (`bot/**` etc), so the hook must match +# the same way. +# +# Fail-closed on $CLAUDE_PROJECT_DIR — if unset, no bypass and no rooted +# matching (no $PWD fallback, since $PWD can be agent-controlled whereas +# CLAUDE_PROJECT_DIR is set by the Claude Code harness from the session's +# project root). When unset, we strip a leading `/` so absolute paths still +# enter the relative-pattern case, and rely on the literal pattern strings. +PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-}" +if [ -n "$PROJECT_ROOT" ] && [[ "$FILE_PATH" == "$PROJECT_ROOT"/* ]]; then + REL_PATH="${FILE_PATH#"$PROJECT_ROOT"/}" +else + REL_PATH="${FILE_PATH#/}" fi +# --- 1. Skills — cron-only block (interactive sessions can still edit) --- +case "$REL_PATH" in + .claude/skills/*) + if [ -n "$CRON_NAME" ]; then + echo "Blocked: cron '$CRON_NAME' cannot modify skill files: $FILE_PATH" >&2 + exit 2 + fi + ;; +esac + # --- 2. Upstream-owned platform files — block ALL sessions --- # Mirror of `bot-code-readonly.md` paths frontmatter. Keep these two lists # in lockstep — the rule is the doc, the hook is the enforcement. If you @@ -60,11 +80,6 @@ fi # 1. PROTECT_FILES_BYPASS=1 — explicit opt-out for one-off cases # 2. $CLAUDE_PROJECT_DIR contains `/.ralphex/worktrees/` — ralphex pipeline # 3. git remote.origin.url at $CLAUDE_PROJECT_DIR is the upstream repo -# -# Fail-closed on $CLAUDE_PROJECT_DIR — if unset, no bypass (no $PWD fallback, -# since $PWD can be an agent-controlled location whereas CLAUDE_PROJECT_DIR -# is set by the Claude Code harness from the session's project root). -PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-}" bypass="" if [ "${PROTECT_FILES_BYPASS:-0}" = "1" ]; then @@ -87,17 +102,17 @@ if [ -n "$bypass" ]; then exit 0 fi -case "$FILE_PATH" in - */bot/*|bot/*) match=1 ;; - */.claude/hooks/*|.claude/hooks/*) match=1 ;; - */.claude/rules/platform/*|.claude/rules/platform/*) match=1 ;; - */.claude/skills/workspace-health/scripts/*|.claude/skills/workspace-health/scripts/*) match=1 ;; - */.github/workflows/*|.github/workflows/*) match=1 ;; - */.githooks/*|.githooks/*) match=1 ;; - */.gitleaks.toml|.gitleaks.toml) match=1 ;; - */.gitleaksignore|.gitleaksignore) match=1 ;; - */README.md|README.md) match=1 ;; - */config.local.yaml.example|config.local.yaml.example) match=1 ;; +case "$REL_PATH" in + bot/*) match=1 ;; + .claude/hooks/*) match=1 ;; + .claude/rules/platform/*) match=1 ;; + .claude/skills/workspace-health/scripts/*) match=1 ;; + .github/workflows/*) match=1 ;; + .githooks/*) match=1 ;; + .gitleaks.toml) match=1 ;; + .gitleaksignore) match=1 ;; + README.md) match=1 ;; + config.local.yaml.example) match=1 ;; *) match=0 ;; esac From ad63baf7ae70e15b9d02cbb9d672b59a161071e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 22:34:56 +0000 Subject: [PATCH 5/5] test: stabilize protect-files hook tests with explicit project root env Agent-Logs-Url: https://github.com/fitz123/claude-code-bot/sessions/c0947a7f-0ad9-4031-ba90-7eb97358feb9 Co-authored-by: fitz123 <10243861+fitz123@users.noreply.github.com> --- bot/src/__tests__/safety-hooks.test.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/bot/src/__tests__/safety-hooks.test.ts b/bot/src/__tests__/safety-hooks.test.ts index cd24a14..b62a6c1 100644 --- a/bot/src/__tests__/safety-hooks.test.ts +++ b/bot/src/__tests__/safety-hooks.test.ts @@ -46,11 +46,13 @@ function runHookRaw( // ------------------------------------------------------------------- describe("protect-files.sh", () => { + const PROTECT_ENV = { CLAUDE_PROJECT_DIR: "/workspace" }; + it("allows when CRON_NAME is not set", () => { const result = runHook(PROTECT_FILES, { tool_name: "Edit", tool_input: { file_path: "/workspace/.claude/skills/foo/SKILL.md" }, - }); + }, PROTECT_ENV); assert.equal(result.exitCode, 0); }); @@ -61,7 +63,7 @@ describe("protect-files.sh", () => { tool_name: "Write", tool_input: { file_path: "/workspace/memory/notes.md" }, }, - { CRON_NAME: "nightly-consolidation" }, + { ...PROTECT_ENV, CRON_NAME: "nightly-consolidation" }, ); assert.equal(result.exitCode, 0); }); @@ -75,7 +77,7 @@ describe("protect-files.sh", () => { file_path: "/workspace/.claude/skills/workspace-health/SKILL.md", }, }, - { CRON_NAME: "nightly-consolidation" }, + { ...PROTECT_ENV, CRON_NAME: "nightly-consolidation" }, ); assert.equal(result.exitCode, 2); assert.ok(result.stderr.includes("Blocked")); @@ -91,7 +93,7 @@ describe("protect-files.sh", () => { file_path: "/workspace/.claude/skills/workspace-health/SKILL.md", }, }, - { CRON_NAME: "nightly-consolidation" }, + { ...PROTECT_ENV, CRON_NAME: "nightly-consolidation" }, ); assert.equal(result.exitCode, 2); assert.ok(result.stderr.includes("Blocked")); @@ -102,12 +104,12 @@ describe("protect-files.sh", () => { const result = runHook(PROTECT_FILES, { tool_name: "Write", tool_input: {}, - }); + }, PROTECT_ENV); assert.equal(result.exitCode, 0); }); it("blocks on malformed JSON input (fail-closed)", () => { - const result = runHookRaw(PROTECT_FILES, "{"); + const result = runHookRaw(PROTECT_FILES, "{", PROTECT_ENV); assert.equal(result.exitCode, 2); assert.ok(result.stderr.includes("failed to parse")); }); @@ -122,7 +124,7 @@ describe("protect-files.sh", () => { "/workspace/.claude/./skills/workspace-health/SKILL.md", }, }, - { CRON_NAME: "nightly-consolidation" }, + { ...PROTECT_ENV, CRON_NAME: "nightly-consolidation" }, ); assert.equal(result.exitCode, 2); assert.ok(result.stderr.includes("Blocked")); @@ -138,7 +140,7 @@ describe("protect-files.sh", () => { "/workspace/.claude/notskills/../skills/workspace-health/SKILL.md", }, }, - { CRON_NAME: "nightly-consolidation" }, + { ...PROTECT_ENV, CRON_NAME: "nightly-consolidation" }, ); assert.equal(result.exitCode, 2); assert.ok(result.stderr.includes("Blocked")); @@ -153,7 +155,7 @@ describe("protect-files.sh", () => { file_path: ".claude/skills/workspace-health/SKILL.md", }, }, - { CRON_NAME: "nightly-consolidation" }, + { ...PROTECT_ENV, CRON_NAME: "nightly-consolidation" }, ); assert.equal(result.exitCode, 2); assert.ok(result.stderr.includes("Blocked")); @@ -169,7 +171,7 @@ describe("protect-files.sh", () => { "/workspace/.claude//skills/workspace-health/SKILL.md", }, }, - { CRON_NAME: "nightly-consolidation" }, + { ...PROTECT_ENV, CRON_NAME: "nightly-consolidation" }, ); assert.equal(result.exitCode, 2); assert.ok(result.stderr.includes("Blocked"));