feat: Add Claude Code settings.json composition support#50
feat: Add Claude Code settings.json composition support#50westonplatter wants to merge 2 commits intomainfrom
Conversation
…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
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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.
| 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, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Unreachable code: ClaudeSettings entries are cataloged as CompositeAgentsMd.
This code block is unreachable because:
validate_manifestrequiressourcesto be non-empty forClaudeSettingsentriesEntry::is_composite()returnstrueforClaudeSettingswhensourcesis non-empty (seesrc/manifest.rslines 72-73)- 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.
Summary
This PR adds support for composing Claude Code
settings.jsonfiles 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
claude_settingsasset kind: Added support for a new entry type that composes permissions from multiple YAML sources into a single JSON filesrc/claude_settings.rs): Implements the core logic for:allowanddenylists)settings.jsonformatinstall_claude_settings_entry()function to handle the composition and writing of settings files during syncsourcesarray forclaude_settingsentriesclaude_settingsentries in the catalogserde_jsondependency for JSON serializationImplementation Details
BTreeSetfor automatic deduplication and alphabetical sortingExample Usage
The composed output will be a valid
settings.jsonwith merged and deduplicated permissions.https://claude.ai/code/session_01AiCUb4qeFNCdro47Kz9W9C
Summary by CodeRabbit
Release Notes
claude_settingsasset kind enables automated permission management.claude/settings.json✏️ Tip: You can customize this high-level summary in your review settings.