Skip to content

feat: Add Claude Code settings.json composition support#50

Open
westonplatter wants to merge 2 commits intomainfrom
claude/local-permissions-config-ggGQE
Open

feat: Add Claude Code settings.json composition support#50
westonplatter wants to merge 2 commits intomainfrom
claude/local-permissions-config-ggGQE

Conversation

@westonplatter
Copy link
Copy Markdown
Owner

@westonplatter westonplatter commented Jan 27, 2026

Summary

This PR adds support for composing Claude Code settings.json files from multiple YAML permission fragments. This enables teams to define permissions in separate, manageable YAML files that are automatically merged into a single settings file during sync.

Key Changes

  • New claude_settings asset kind: Added support for a new entry type that composes permissions from multiple YAML sources into a single JSON file
  • Permission composition module (src/claude_settings.rs): Implements the core logic for:
    • Reading permission fragments from YAML files (with allow and deny lists)
    • Merging multiple fragments using set union operations
    • Removing denied entries from the allow list
    • Sorting and deduplicating results for deterministic output
    • Serializing to Claude Code's settings.json format
  • Installation support: Added install_claude_settings_entry() function to handle the composition and writing of settings files during sync
  • Manifest validation: Extended validation to require sources array for claude_settings entries
  • Catalog support: Added enumeration of claude_settings entries in the catalog
  • Dependencies: Added serde_json dependency for JSON serialization

Implementation Details

  • Merge strategy: Uses BTreeSet for automatic deduplication and alphabetical sorting
  • Conflict handling: Respects existing conflict detection and backup mechanisms
  • Checksum tracking: Computes checksums of composed content for change detection
  • Dry-run support: Fully integrated with existing dry-run functionality
  • Comprehensive tests: Added 8 integration tests covering single/multiple sources, deny lists, deduplication, idempotency, and validation

Example Usage

entries:
  - id: claude-perms
    kind: claude_settings
    sources:
      - type: filesystem
        root: ./permissions
        path: shared.yaml
      - type: filesystem
        root: ./permissions
        path: local.yaml
    dest: .claude/settings.json

The composed output will be a valid settings.json with merged and deduplicated permissions.

https://claude.ai/code/session_01AiCUb4qeFNCdro47Kz9W9C

Summary by CodeRabbit

Release Notes

  • New Features
    • Added support for composing Claude Code settings from multiple permission sources
    • New claude_settings asset kind enables automated permission management
    • Settings support allow/deny lists with automatic merging, deduplication, and conflict resolution
    • Default output location: .claude/settings.json

✏️ Tip: You can customize this high-level summary in your review settings.

…sions

Add a new `claude_settings` asset kind that composes multiple YAML
permission fragments into a single Claude Code `.claude/settings.json`
file. This enables managing permissions from multiple sources (shared
team configs + local personal overrides) using the existing APS
composition pattern.

Key behaviors:
- Each source provides a YAML file with `allow` and/or `deny` lists
- Merge strategy: union all entries, deduplicate, sort alphabetically
- Deny entries are removed from allow list to prevent conflicts
- Output is valid Claude Code settings.json with permissions object

New module: src/claude_settings.rs
- PermissionFragment parsing from YAML
- compose_permissions() for N-way merge with deny filtering
- JSON output generation via serde_json

Includes 10 unit tests and 8 integration tests covering composition,
deduplication, deny filtering, idempotency, and validation.

https://claude.ai/code/session_01AiCUb4qeFNCdro47Kz9W9C
@westonplatter westonplatter changed the title Add Claude Code settings.json composition support feat: Add Claude Code settings.json composition support Feb 1, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 1, 2026

📝 Walkthrough

Walkthrough

This PR introduces comprehensive Claude Settings support, enabling composition of multiple YAML permission fragments into a deterministic Claude Code settings.json file. The implementation includes a new module for permissions handling, manifest support for a ClaudeSettings asset kind, a dedicated installation workflow with checksumming and conflict detection, error handling, and extensive test coverage across the codebase.

Changes

Cohort / File(s) Summary
Core Claude Settings Module
src/claude_settings.rs
New module composing multiple YAML permission fragments into Claude Code settings.json, including PermissionFragment, ComposedPermissions, and ClaudeSettingsOutput structs; provides read_permission_fragment, compose_permissions, and write_settings_file functions with comprehensive tests for sorting, deduplication, denial interaction, and file I/O.
Manifest & Asset Kind Support
src/manifest.rs
Introduces ClaudeSettings as a composite-like AssetKind with default destination .claude/settings.json; updates is_composite logic to recognize ClaudeSettings with sources; adds string deserialization for claude_settings.
Installation & Catalog Integration
src/install.rs, src/catalog.rs
Adds install_claude_settings_entry workflow validating sources, reading and checksumming fragments, composing payload, resolving destinations, detecting unchanged content, handling conflicts, and writing to file; catalog generation includes ClaudeSettings entries with :settings suffix.
Command Routing & Error Handling
src/commands.rs, src/error.rs, src/main.rs
Routes ClaudeSettings entries to dedicated install_claude_settings_entry handler in sync flow (checked before composite); adds ClaudeSettingsError variant for composition failures; adds module declaration for claude_settings.
Dependencies
Cargo.toml
Adds serde_json = "1" dependency for JSON serialization.
Test Coverage
tests/cli.rs
Comprehensive tests covering single/multiple permission sources, merging and deduplication, deny-list interaction with allow-list, idempotent runs, default destination behavior, validation of claude_settings entries, missing source error handling, and upstream integrity checks.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI User
    participant Commands as commands.rs
    participant Install as install.rs
    participant Settings as claude_settings.rs
    participant FS as File System

    CLI->>Commands: sync (ClaudeSettings entry)
    Commands->>Install: install_claude_settings_entry(entry)
    
    Install->>FS: resolve source paths
    Install->>FS: read fragment 1
    FS-->>Install: YAML content
    Install->>Settings: read_permission_fragment()
    Settings-->>Install: PermissionFragment {allow, deny}
    
    Install->>FS: read fragment 2
    FS-->>Install: YAML content
    Install->>Settings: read_permission_fragment()
    Settings-->>Install: PermissionFragment {allow, deny}
    
    Install->>Settings: compose_permissions(fragments)
    Settings->>Settings: merge allows (BTreeSet)
    Settings->>Settings: merge denies (BTreeSet)
    Settings->>Settings: remove denied from allow
    Settings->>Settings: serialize to JSON
    Settings-->>Install: JSON string + checksum
    
    Install->>FS: check lockfile for existing checksum
    FS-->>Install: old checksum (if exists)
    
    alt Content unchanged
        Install-->>Commands: up-to-date result
    else Content changed or new
        Install->>FS: handle conflicts (backup if needed)
        Install->>Settings: write_settings_file()
        Settings->>FS: create parent dirs + write JSON
        FS-->>Settings: success
        Settings-->>Install: ok
        Install->>FS: update lockfile with new checksum
        Install-->>Commands: installed result
    end
    
    Commands-->>CLI: sync complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hop along through fragments fine,
Permission lists now intertwine,
Claude's settings JSON, clean and neat,
Merged with care, deduped sweet!
Composing dreams in files so bright,
Our burrow's code now feels just right!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: Add Claude Code settings.json composition support' directly summarizes the main change: adding support for composing Claude Code settings.json files from multiple YAML permission fragments, which is the core objective of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/local-permissions-config-ggGQE

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/catalog.rs`:
- Around line 268-277: The cataloging path incorrectly treats ClaudeSettings as
CompositeAgentsMd because Entry::is_composite() (as enforced by
validate_manifest requiring non-empty sources) causes ClaudeSettings to hit the
early composite handling branch; to fix, move the ClaudeSettings-specific
handling ahead of the generic composite check so ClaudeSettings entries are
emitted with kind AssetKind::ClaudeSettings and the correct name/destination,
and then remove the now-unreachable match arm for AssetKind::ClaudeSettings in
the catalog entry match to avoid duplicate/incorrect metadata.

Comment thread src/catalog.rs
Comment on lines +268 to +277
AssetKind::ClaudeSettings => {
// Claude settings entries use composite sources (handled above for composites)
catalog_entries.push(CatalogEntry {
id: format!("{}:settings", entry.id),
name: "settings.json (composed)".to_string(),
kind: AssetKind::ClaudeSettings,
destination: format!("./{}", base_dest.display()),
short_description: None,
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unreachable code: ClaudeSettings entries are cataloged as CompositeAgentsMd.

This code block is unreachable because:

  1. validate_manifest requires sources to be non-empty for ClaudeSettings entries
  2. Entry::is_composite() returns true for ClaudeSettings when sources is non-empty (see src/manifest.rs lines 72-73)
  3. The early return at lines 135-144 catches all composite entries and returns with kind: AssetKind::CompositeAgentsMd

As a result, ClaudeSettings entries will be cataloged with incorrect metadata (kind=CompositeAgentsMd, name="AGENTS.md (composite)").

🐛 Proposed fix: Handle ClaudeSettings before the generic composite check
 fn enumerate_entry_assets(entry: &Entry, manifest_dir: &Path) -> Result<Vec<CatalogEntry>> {
     let base_dest = entry.destination();
     let mut catalog_entries = Vec::new();
 
+    // Handle ClaudeSettings entries (composite-like but distinct catalog representation)
+    if entry.kind == AssetKind::ClaudeSettings {
+        catalog_entries.push(CatalogEntry {
+            id: format!("{}:settings", entry.id),
+            name: "settings.json (composed)".to_string(),
+            kind: AssetKind::ClaudeSettings,
+            destination: format!("./{}", base_dest.display()),
+            short_description: Some(format!("Composed from {} sources", entry.sources.len())),
+        });
+        return Ok(catalog_entries);
+    }
+
     // Handle composite entries (no single source to resolve)
     if entry.is_composite() {

Then remove the unreachable AssetKind::ClaudeSettings match arm at lines 268-277.

🤖 Prompt for AI Agents
In `@src/catalog.rs` around lines 268 - 277, The cataloging path incorrectly
treats ClaudeSettings as CompositeAgentsMd because Entry::is_composite() (as
enforced by validate_manifest requiring non-empty sources) causes ClaudeSettings
to hit the early composite handling branch; to fix, move the
ClaudeSettings-specific handling ahead of the generic composite check so
ClaudeSettings entries are emitted with kind AssetKind::ClaudeSettings and the
correct name/destination, and then remove the now-unreachable match arm for
AssetKind::ClaudeSettings in the catalog entry match to avoid
duplicate/incorrect metadata.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants