You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
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.
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.
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.yamlversion: 1exceptions:
- id: alice-local-postgresselector:
agent: claudescope: userkind: mcp_servername: local-postgresreason: Temporary debugging against local database replicaowner: alice@example.comexpires: 2026-07-31
- id: codex-profile-workbenchselector:
agent: codexscope: userkind: native_fieldpath: profiles.workbenchreason: Local model/provider experiment, not part of shared baselineowner: alice@example.comexpires: 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:
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.
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
Add destination scanners for known co-owned files: Codex config.toml, Claude .claude.json / project .mcp.json, and Claude settings.json.
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.
Add .kanon/exceptions.yaml structs, validation, exact selector matching, and expiry parsing.
Add kanon exceptions list and kanon exceptions check --policy warn|fail with text and JSON output.
Keep outputs content-free: no secret values, no full header values, no instruction/skill bodies.
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.
🤖 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:
kanon.yaml?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:
README.md:124-128).config.tomland Claude.claude.json/ project.mcp.jsonpreserve other fields and server entries, while Claudesettings.jsonpreserves settings outside the renderedhookssection (README.md:190-195).MergeRenderedContentdispatches Codex config tomergeTOMLTable, Claude MCP JSON tomergeJSONMapField, and Claude settings tomergeJSONFields(internal/core/merge.go:12-21).mergeTOMLTableloads the existing table and only overwrites names present in the desired table, so extramcp_serversentries remain (internal/core/merge.go:43-54).mergeJSONMapFielddoes the same for JSON map fields such asmcpServers(internal/core/merge.go:77-94).privatesurvives apply (internal/core/apply_test.go:92-131).privateand an unmanagedautoUpdatessetting survive apply (internal/core/apply_test.go:368-411).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:
local-postgresstdio 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.kanon exceptions checkunless it has a named exception./home/student01,/home/student02, or a runner account for unmanaged preserved settings before applying the shared source.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:
ignore_changesfor attributes intentionally managed outside Terraform, so ignored drift is declared rather than accidental: https://developer.hashicorp.com/terraform/language/meta-arguments/lifecyclePolicyExceptionresources for explicitly exempting selected resources from selected policy rules: https://kyverno.io/docs/guides/exceptions/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: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 Codexmcp_servers.*or ClaudemcpServers.*entries not selected fromkanon.yamlfor that target.native_field: high-impact native fields Kanon preserves but does not manage, such as Codexprofiles.*, Codex top-level provider/model/approval fields, or Claudepermissions/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 (
executablefor local-command MCP,permissionfor 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-31Suggested output:
Suggested behavior:
exceptions listprints detected unmanaged settings and matching exception status.exceptions check --policy warn|failexits non-zero only infailmode when unmanaged settings lack a valid exception or have expired exceptions.kanon statuscan later summarize “N unmanaged preserved setting(s), M unapproved” without changing default status semantics in the first slice.applyshould keep current behavior by default. A laterapply --unmanaged-policy failcan reuse the same checker for stricter rollouts.Example team policy
A security-conscious team can add this to its shared repo CI or admin rollout script:
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
config.toml, Claude.claude.json/ project.mcp.json, and Claudesettings.json.mcp_server, selected high-impactnative_field, and optionallyskill_directoryitems..kanon/exceptions.yamlstructs, validation, exact selector matching, and expiry parsing.kanon exceptions listandkanon exceptions check --policy warn|failwith text and JSON output.Backward compatibility
kanon.yamlfiles are unchanged..kanon/exceptions.yamlis optional and lives under an already-gitignored directory.Non-goals