feat: non-destructive config generation (v2.37.0)#17
Merged
Conversation
Make instruction-doc regeneration and permission-tier application non-destructive: C3 now owns only its own slice and preserves user-written content everywhere. Instruction docs (CLAUDE.md / AGENTS.md / GEMINI.md): wrap C3 content in C3:BEGIN / C3:END comment markers; merge instead of overwrite across save_claude_md, the Hub save endpoint, and `c3 claudemd save`; ClaudeMdManager.compact() now compacts only the inner block and re-wraps it so the markers and surrounding user content survive. settings.local.json: permission tiers now merge into existing permissions (preserving user-added allow/deny plus keys like ask and defaultMode) across all four appliers (_apply_permission_tier, install-mcp --permissions, per-project server, and Hub). install-mcp now preserves user-added Stop hooks, replacing only C3's own. .mcp.json was already safe (only the c3 server entry is replaced). Bump version to 2.37.0, add a CHANGELOG entry, document the behavior in README and the in-app guide, and add tests (test_claude_md_merge.py, permission-merge cases, and an install-mcp preservation test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR makes C3’s project config generation non-destructive by ensuring regenerated instruction docs and permission-tier applications only replace C3-managed content while preserving user-authored content.
Changes:
- Introduces C3-managed sentinel blocks for instruction docs and routes all write paths through shared merge/write helpers.
- Adds permission-tier merging to preserve user-added
allow/denyentries and non-list permission keys while replacing only C3-managed tier entries. - Expands test coverage for instruction-doc merging/compaction and install-mcp preservation behavior; bumps version to 2.37.0 and updates docs/changelog.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
services/claude_md.py |
Adds managed-block sentinels + merge/write helpers; updates compact() to operate on the managed inner block. |
services/session_manager.py |
Switches instruction doc writes to the new non-destructive writer. |
cli/commands/common.py |
Routes c3 claudemd save through the non-destructive writer. |
cli/server.py |
Updates Hub save and permission-tier apply to merge rather than overwrite. |
cli/hub_server.py |
Updates per-project permission-tier apply to merge rather than overwrite. |
cli/c3.py |
Adds _merge_permission_tier and Stop-hook preservation logic; bumps CLI version. |
tests/test_claude_md_merge.py |
New tests covering managed-block wrapping/merging and compaction preservation. |
tests/test_permissions.py |
Updates tests to validate permission merging and tier switching behavior. |
tests/test_install_mcp_entrypoint.py |
Adds install-mcp test ensuring user config (servers, permissions, hooks) is preserved. |
README.md |
Documents managed-block behavior and new permission-tier merge semantics. |
cli/guide/getting-started.html |
Updates guide to describe non-destructive merges for instruction docs, hooks, and permissions. |
pyproject.toml |
Bumps package version to 2.37.0. |
CHANGELOG.md |
Adds 2.37.0 entry describing non-destructive generation behavior. |
CLAUDE.md |
Updates project structure listing content. |
Comment on lines
+109
to
+116
| # 1. Markers present → surgical in-place replacement. | ||
| if C3_BLOCK_BEGIN in existing and C3_BLOCK_END in existing: | ||
| start = existing.index(C3_BLOCK_BEGIN) | ||
| end = existing.index(C3_BLOCK_END) + len(C3_BLOCK_END) | ||
| before = existing[:start].rstrip() | ||
| after = existing[end:].lstrip() | ||
| parts = [p for p in (before, new_block, after) if p] | ||
| return "\n\n".join(parts) + "\n" |
Comment on lines
+396
to
+404
| has_block = C3_BLOCK_BEGIN in current and C3_BLOCK_END in current | ||
| if has_block: | ||
| start = current.index(C3_BLOCK_BEGIN) | ||
| end = current.index(C3_BLOCK_END) + len(C3_BLOCK_END) | ||
| before = current[:start] | ||
| after = current[end:] | ||
| inner = current[start + len(C3_BLOCK_BEGIN):end - len(C3_BLOCK_END)].strip() | ||
| if inner.startswith(C3_BLOCK_HEADING): | ||
| inner = inner[len(C3_BLOCK_HEADING):].lstrip("\n") |
Comment on lines
+409
to
+414
| pieces = [] | ||
| if before.strip(): | ||
| pieces.append(before.strip()) | ||
| pieces.append(wrap_c3_block(compacted_inner)) | ||
| if after.strip(): | ||
| pieces.append(after.strip()) |
Comment on lines
+8
to
+12
| import tempfile | ||
| import unittest | ||
| from pathlib import Path | ||
|
|
||
| from services.claude_md import ( |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes C3's config generation non-destructive. The rule everywhere is now "C3 owns only its own slice; anything you wrote by hand is preserved." Previously, regenerating instruction docs or applying a permission tier could silently clobber user content.
What changed
Instruction docs (
CLAUDE.md/AGENTS.md/GEMINI.md)<!-- C3:BEGIN … -->/<!-- C3:END -->sentinels with a visible# C3 — Managed Instructionsheading.services/claude_md.py:wrap_c3_block,merge_c3_block,write_c3_instruction_doc.SessionManager.save_claude_md(install/init),api_claudemd_save(Hub),cmd_claudemd(c3 claudemd save).## C3 Tools) → replace head, keep trailing# User Notes; genuine user file → append below, never destroy.ClaudeMdManager.compact()now compacts only the inner block and re-wraps it, so the markers and surrounding user content survive (logic split intocompact+_compact_sections).settings.local.json_merge_permission_tier+_c3_managed_permission_entriespreserve user-addedallow/denyand keys likeask/defaultMode, replacing only the entries a tier manages. Wired into all four appliers:_apply_permission_tier,cmd_install_mcp --permissions,cli/server.py:api_permissions_put,cli/hub_server.py:api_projects_permissions_put.install-mcpnow identifies C3's ownStophooks by their hook scripts and replaces only those — user-added stop hooks (including the common matcher-less shape) survive. Post/PreToolUse hooks were already merged by matcher..mcp.json_upsert_json_mcp_serveronly replaces thec3server). Covered by a new test.Tradeoff
This intentionally flips the previous "permissions replaced wholesale" contract. Tier
denyrules still apply authoritatively (and deny wins over allow), but a user-customallownow persists across tier switches. The authoritative tier is still stored in.c3/config.jsonand surfaced byc3 permissions show.Version & docs
pyproject.toml,cli/c3.py) + CHANGELOG entry.README.mdand the in-app guide (cli/guide/getting-started.html).Tests
tests/test_claude_md_merge.py(wrap / in-place replace / legacy migration / user-file append / compact preservation).tests/test_permissions.py(custom rules +askpreserved; stale tier entries dropped; tier-switch behavior).tests/test_install_mcp_entrypoint.py(.mcp.jsonservers + user permissions + custom PostToolUse hook + empty-matcher Stop hook all survive).c3_validateclean on all edited Python files.🤖 Generated with Claude Code