Skip to content

Add managed exceptions for preserved local agent settings #98

Description

@kelos-bot

🤖 Kelos Strategist Agent @gjkim42

Area: New Managed-Settings Types & CLI Extensions

Summary

Kanon intentionally preserves destination settings it does not own. That is the right default for co-owned agent files, but it leaves teams without a way to see, justify, or expire local deviations from the central Kanon source.

Today a developer can add a local MCP server, Claude permission block, Codex profile, or other native setting directly to destination state. Kanon may keep applying the shared baseline and still report no rendered drift, because the merge layer preserves those unmanaged entries by design. For individual use that is friendly. For a team-managed source repo it becomes a blind spot:

  • Which destination settings are present but not declared in kanon.yaml?
  • Which local MCP servers or permission settings are intentionally allowed?
  • Which exceptions were temporary and should be removed?
  • Can CI or an admin-run rollout fail when unmanaged executable config appears?

Proposal: add a small unmanaged-setting inventory plus a managed exception file, so Kanon can keep preserving co-owned config while making non-source state visible and governable.

Evidence from the current codebase

Kanon already documents and tests this preservation behavior:

  • The README says files not rendered by the current source are outside the apply plan; Kanon does not scan, list, delete, or keep destination state for them (README.md:124-128).
  • The README also says co-owned config files are merged: Codex config.toml and Claude .claude.json / project .mcp.json preserve other fields and server entries, while Claude settings.json preserves settings outside the rendered hooks section (README.md:190-195).
  • MergeRenderedContent dispatches Codex config to mergeTOMLTable, Claude MCP JSON to mergeJSONMapField, and Claude settings to mergeJSONFields (internal/core/merge.go:12-21).
  • mergeTOMLTable loads the existing table and only overwrites names present in the desired table, so extra mcp_servers entries remain (internal/core/merge.go:43-54).
  • mergeJSONMapField does the same for JSON map fields such as mcpServers (internal/core/merge.go:77-94).
  • Tests assert that an unmanaged Codex MCP server named private survives apply (internal/core/apply_test.go:92-131).
  • Tests assert that an unmanaged Claude MCP server named private and an unmanaged autoUpdates setting survive apply (internal/core/apply_test.go:368-411).
  • Tests also assert that stopped-rendering files are not pruned (internal/core/apply_test.go:64-90).

Net: preservation is not accidental. The missing product layer is not “delete local changes”; it is “make preserved local changes observable and explicitly allowed when teams care.”

Why this matters strategically

This unlocks a new governance use case without weakening individual workflows: teams can centrally manage a baseline while still allowing controlled local additions.

Concrete examples:

  1. Local database MCP server: a backend developer needs a local-postgres stdio MCP server for two weeks. Kanon should allow it with an expiry and reason, not hide it forever or force it into the team baseline.
  2. Contractor laptop policy: a platform repo manages approved remote MCP servers, but any unmanaged local-command MCP server should fail kanon exceptions check unless it has a named exception.
  3. Security incident response: a team wants to ask, “which machines still have unmanaged Claude permissions or extra MCP servers?” without collecting raw prompts or secrets.
  4. Shared hosts and service accounts: this composes with destination-home targeting (Add explicit destination-home targeting for shared hosts and service-account runners #95): an admin can scan /home/student01, /home/student02, or a runner account for unmanaged preserved settings before applying the shared source.
  5. Gradual adoption: brownfield users can keep local settings while a team migrates them into kanon.yaml, but the local leftovers become visible and time-bounded instead of silent.

External signal

Adjacent config systems generally distinguish owned state from allowed out-of-band state:

Kanon does not need their complexity. The useful pattern is smaller: when unmanaged state is preserved, the owner should be able to inventory it and optionally require a specific exception record.

Proposed model

Add an optional local exception file under the already-gitignored .kanon/ directory:

# <kanon-home>/.kanon/exceptions.yaml
version: 1
exceptions:
  - id: alice-local-postgres
    selector:
      agent: claude
      scope: user
      kind: mcp_server
      name: local-postgres
    reason: Temporary debugging against local database replica
    owner: alice@example.com
    expires: 2026-07-31

  - id: codex-profile-workbench
    selector:
      agent: codex
      scope: user
      kind: native_field
      path: profiles.workbench
    reason: Local model/provider experiment, not part of shared baseline
    owner: alice@example.com
    expires: 2026-07-15

Keep v1 local and gitignored because these are destination-specific facts. A later source-level policy can add committed allowlists if teams need org-wide exceptions, but the first useful step is machine-local visibility and expiry.

Suggested detected unmanaged kinds for v1:

  • mcp_server: extra Codex mcp_servers.* or Claude mcpServers.* entries not selected from kanon.yaml for that target.
  • native_field: high-impact native fields Kanon preserves but does not manage, such as Codex profiles.*, Codex top-level provider/model/approval fields, or Claude permissions / autoUpdates.
  • skill_directory: destination skill directories that are not rendered by the current source, reported as inventory only at first because stopped-rendering files are currently outside the apply plan.

Each inventory item should include content-free facts only: agent, scope, kind, path/name, destination file, rough risk (executable for local-command MCP, permission for permission fields), and whether a matching exception is present/expired.

CLI proposal

Add a command group that scans destination state against the current source and rendered target selection:

kanon exceptions list
kanon exceptions check --policy warn
kanon exceptions check --policy fail
kanon exceptions add mcp_server claude local-postgres \
  --reason "Temporary local DB debugging" \
  --owner alice@example.com \
  --expires 2026-07-31

Suggested output:

kind         agent   name/path              status      destination
mcp_server   claude  local-postgres         allowed     ~/.claude.json
mcp_server   codex   private-github         untracked   ~/.codex/config.toml
native_field claude  permissions            expired     ~/.claude/settings.json

Suggested behavior:

  • exceptions list prints detected unmanaged settings and matching exception status.
  • exceptions check --policy warn|fail exits non-zero only in fail mode when unmanaged settings lack a valid exception or have expired exceptions.
  • kanon status can later summarize “N unmanaged preserved setting(s), M unapproved” without changing default status semantics in the first slice.
  • apply should keep current behavior by default. A later apply --unmanaged-policy fail can reuse the same checker for stricter rollouts.
  • Exception matching must be exact enough to avoid blanket bypasses in v1. Prefer agent + scope + kind + name/path; defer glob patterns until real demand exists.

Example team policy

A security-conscious team can add this to its shared repo CI or admin rollout script:

kanon validate
kanon lock check
kanon exceptions check --policy fail
kanon apply --yes

Developers keep the ability to run personal MCP servers or native settings locally, but high-impact deviations are no longer invisible. They either migrate into kanon.yaml, get a local exception with an expiry, or fail the stricter team workflow.

Existing issue overlap check

I reviewed open generated issues and searched all issues for unmanaged/exception/local override overlap before filing. I did not find a focused issue for preserved destination exceptions.

This is distinct from:

It also avoids the excluded lanes: it is not self-development prompt tuning, not documentation/wording polish, and not agent configuration based on PR reviews.

Suggested MVP scope

  1. Add destination scanners for known co-owned files: Codex config.toml, Claude .claude.json / project .mcp.json, and Claude settings.json.
  2. Compare scanned entries against the current rendered/source-selected settings to identify unmanaged mcp_server, selected high-impact native_field, and optionally skill_directory items.
  3. Add .kanon/exceptions.yaml structs, validation, exact selector matching, and expiry parsing.
  4. Add kanon exceptions list and kanon exceptions check --policy warn|fail with text and JSON output.
  5. Keep outputs content-free: no secret values, no full header values, no instruction/skill bodies.
  6. Add tests for extra MCP server detection, matching exceptions, expired exceptions, parse errors in co-owned files, project-scoped paths, and no-output behavior when destination equals source.

Backward compatibility

  • Existing kanon.yaml files are unchanged.
  • Existing render/diff/apply/update defaults are unchanged.
  • .kanon/exceptions.yaml is optional and lives under an already-gitignored directory.
  • Preservation remains the default merge behavior.
  • Strict enforcement is opt-in through the new check command or a later explicit policy flag.

Non-goals

  • Do not delete unmanaged settings in v1.
  • Do not collect or upload destination contents.
  • Do not create broad glob exceptions in the first implementation.
  • Do not turn Kanon into a full endpoint inventory tool; scan only agent settings Kanon already knows how to render or preserve.
  • Do not make every individual user maintain exceptions unless they opt into a stricter team workflow.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions