Transpile a neutral agent configuration into Claude Code, Codex CLI, and other agent-specific formats — and back again.
Project description
Chameleon
One neutral config for Claude Code and Codex CLI, with round-trip merge in both directions.
Chameleon keeps a repo-backed neutral YAML file as the source of truth for your AI coding setup, but it does not pretend the live agent configs are read-only. Claude and Codex can still mutate their own files at runtime. On the next merge, Chameleon detects that drift, reconciles it back into neutral, and re-derives every other target.
That makes Chameleon the reconciliation layer between:
- operator-authored neutral config
- Claude Code's live files
- Codex CLI's live files
- login-time background merges
Today Chameleon ships built-in targets for Claude Code and Codex CLI. The plugin system is designed so more targets can join the same loop.
What it manages
Current built-in targets:
- Claude:
~/.claude/settings.jsonand themcpServersslice of~/.claude.json - Codex:
~/.codex/config.tomland~/.codex/requirements.toml
Chameleon stores its own state in XDG locations:
- neutral config:
~/.config/chameleon/neutral.yaml - state root:
~/.local/state/chameleon/ - per-target state repos:
~/.local/state/chameleon/targets/<target>/ - last-known-good neutral snapshot:
~/.local/state/chameleon/neutral.lkg.yaml - operator notices:
~/.local/state/chameleon/notices/
Each target gets a small local git repo. Those repos are not decoration; they are how Chameleon remembers last-applied state, computes drift, restores a target, and keeps a timeline of what was merged.
Why this exists
Claude and Codex overlap enough that maintaining both configs by hand gets old, but they are not the same system.
- Some settings are truly shared and should propagate across tools.
- Some settings are target-specific and should survive round-trip without being flattened away.
- Some settings conflict and need an operator decision.
- Some merges need to happen unattended at login, without blocking your shell.
Chameleon handles those cases with a neutral schema, per-target codecs, pass-through bags for target-only data, and an explicit conflict model instead of silent overwrite.
Core workflow
graph LR
A["neutral.yaml"] --> B["merge"]
C["Claude live files"] --> B
D["Codex live files"] --> B
B --> E["state repos"]
B --> F["re-derived Claude"]
B --> G["re-derived Codex"]
The merge reads four sources for each logical key:
N0: last-known-good neutralN1: current neutral file- per-target live values
- any stored prior resolution for the same conflict path
If everything agrees, the merge is silent. If they do not, Chameleon either:
- accepts the uniquely newest changed source with
latest - resolves interactively on a TTY
- applies a non-interactive strategy
- exits non-zero when unattended resolution is unsafe
Quick start
Chameleon is a uv project. Use uv run, not bare python or pytest.
uv sync
uv run chameleon init
init bootstraps neutral.yaml if missing, then runs a merge with the
equivalent of "keep target-local drift".
Edit the neutral file:
schema_version: 1
identity:
reasoning_effort: high
model:
claude: claude-sonnet-4-7
codex: gpt-5.4
directives:
system_prompt_file: ~/.config/chameleon/AGENTS.md
capabilities:
mcp_servers:
memory:
transport: stdio
command: npx
args:
- -y
- "@modelcontextprotocol/server-memory"
environment:
variables:
CI: "true"
Then apply it:
uv run chameleon merge
Plain chameleon merge defaults to latest: if exactly one newest changed
source can be proven, Chameleon takes it. On an interactive TTY, ambiguous
conflicts fall through to the resolver prompt. Off a TTY, ambiguity exits
non-zero rather than guessing.
What a merge actually does
chameleon merge is not just "render YAML into JSON and TOML".
It:
- Reads live target files.
- Disassembles them back into neutral-shaped fragments.
- Compares those fragments against current neutral and last-known-good neutral.
- Detects conflicts at the neutral-key level.
- Resolves them interactively or non-interactively.
- Re-assembles target files.
- Writes live files and state repos.
- Updates last-known-good neutral.
- Clears stale transaction markers after a clean run.
If nothing changed, the summary is merge: nothing to do.
Conflict behavior
This is the part that matters.
Interactive merge
On a TTY, uv run chameleon merge opens an interactive resolver when needed.
For each conflicting key, it shows:
- last-known-good neutral (
N0) - current neutral (
N1) - each changed target value
You can choose:
- neutral
- one target
- last-known-good
- target-specific preservation
- skip
Interactive decisions are stored in neutral.resolutions. If the same
conflict shape appears again, Chameleon can replay the prior decision instead
of prompting every time. If the values drift enough to invalidate the old
decision hash, it re-prompts and shows the old decision as context.
Non-interactive strategies
For unattended runs:
uv run chameleon merge --on-conflict=latest
uv run chameleon merge --on-conflict=fail
uv run chameleon merge --on-conflict=keep
uv run chameleon merge --on-conflict=prefer-neutral
uv run chameleon merge --on-conflict=prefer-lkg
uv run chameleon merge --on-conflict=prefer=claude
uv run chameleon merge --on-conflict=prefer=codex
Meaning:
latest: take the uniquely newest changed source; fail if ambiguousfail: stop on conflictkeep: leave unresolved cross-target drift in placeprefer-neutral: current neutral winsprefer-lkg: revert to last-known-good neutralprefer=<target>: one target wins
adopt <target> is a convenience wrapper for merge --on-conflict=prefer=<target>.
Login and dotfiles integration
Chameleon is built for login-time sync.
The intended model is:
- dotfiles pull/install happens first
- Chameleon runs last
- clean merges produce no output
- ambiguous conflicts explain themselves before prompting on an interactive shell
The repo ships recipes for:
docs/login/launchd.mddocs/login/systemd.mddocs/login/zlogin.md
The unattended form is:
uv run chameleon merge --on-conflict=latest --quiet --no-warn
For a repo-backed neutral in dotfiles, pass it explicitly:
uv run chameleon merge \
--neutral "$DOTFILES_DIR/chameleon/neutral.yaml" \
--on-conflict=latest \
--quiet \
--no-warn
If a clean merge changes that checked-in neutral file, it is left as a normal dirty working-tree change. Chameleon never commits or pushes for you.
If Git reports a conflict in chameleon/neutral.yaml, resolve the Git conflict
first. Then rerun chameleon merge so the merged neutral spreads to Claude and
Codex.
If Chameleon itself sees an ambiguous conflict on an interactive login, it
prints a short preamble explaining the neutral, Claude, and Codex sources before
asking you to choose. In a non-interactive service, the same ambiguity exits
non-zero and should be resolved by rerunning chameleon merge from a shell.
CLI
init
uv run chameleon init
uv run chameleon init --dry-run
Creates a minimal neutral file if needed, then runs an initial merge.
merge
uv run chameleon merge
uv run chameleon merge --dry-run
uv run chameleon merge --verbose
uv run chameleon merge --quiet --no-warn
uv run chameleon merge --profile deep-review
uv run chameleon merge --on-conflict=prefer=codex
--dry-runemits unified diffs for files it would write--verboseprints state paths, registered targets, pending notices / transactions, per-target warning counts, and merge id--quietsuppresses the summary line and dry-run diff output--no-warnsuppresses end-of-mergeLossWarningerrata--profile <name>applies a named overlay fromprofiles
status
uv run chameleon status
Shows whether neutral exists, whether each target is clean or drifting, and
whether there are pending notices or unresolved transactions. Exit code is 1
if anything is dirty or pending.
diff
uv run chameleon diff
uv run chameleon diff claude
uv run chameleon diff codex
Shows unified diff from state-repo HEAD to the live target file(s). Exit code
matches git-style diff behavior: 0 clean, 1 drift, >1 error.
log
uv run chameleon log claude
uv run chameleon log codex
Shows the per-target state-repo history.
adopt
uv run chameleon adopt claude
uv run chameleon adopt codex
Resolve every conflict in favor of the chosen target, then propagate that back through neutral and out to the other targets.
discard
uv run chameleon discard claude
uv run chameleon discard codex --yes
Restore live target files from the state-repo HEAD. This is intentionally
guarded: off a TTY you must pass --yes.
For partial-owned files, Chameleon only rewrites the keys it owns. Today that
matters for ~/.claude.json, where it preserves keys outside mcpServers.
validate
uv run chameleon validate
Schema-validates neutral.yaml.
doctor
uv run chameleon doctor
uv run chameleon doctor --notices-only
uv run chameleon doctor --clear-notices
Shows Chameleon version, resolved paths, pending operator notices, and unresolved transaction markers from interrupted merges.
targets
uv run chameleon targets list
Lists built-in and plugin-provided targets discovered through the
chameleon.targets entry point.
resolutions
uv run chameleon resolutions list
uv run chameleon resolutions clear
uv run chameleon resolutions clear 'identity.model[claude]' --yes
Inspects or clears stored interactive conflict decisions.
Neutral schema
The neutral schema is centered in src/chameleon/schema/. It has eight
top-level domains:
identitydirectivescapabilitiesauthorizationenvironmentlifecycleinterfacegovernance
Plus:
profiles: named overlays applied withmerge --profile <name>targets.<target>.items: pass-through bag for target-only featuresresolutions: stored conflict decisions
The rule is simple: codecs adapt to the neutral schema. They do not redefine it.
Round-trip guarantees
Round-trip is the design center.
For settings Chameleon claims, the goal is:
from_target(to_target(x)) == canonicalize(x)
When a target has features the other target cannot represent, Chameleon does not silently drop them. It uses one of two escape hatches:
- typed
LossWarnings for genuinely lossy cross-target translations - per-target pass-through under
targets.<target>.items
Examples in the current implementation:
- plugin and marketplace state is reconciled across targets rather than flattened to one side
- target-specific extras can survive under
targets.claude.itemsortargets.codex.items - interrupted merges leave transaction markers instead of disappearing into logs
Repository-backed workflow
The local state repos are a core part of the operator workflow:
statuschecks drift against themdiffshows what changed sinceHEADloggives you a timelinediscardrestores fromHEAD- merges commit new snapshots with a merge id trailer
This gives Chameleon a memory that is stronger than "last file on disk", while still staying local and inspectable.
Plugin targets
New targets register through the chameleon.targets entry point.
At a minimum a plugin provides:
- a
Targetclass - an assembler with file routing
- eight codec classes, one per neutral domain
- vendored
_generated.pymodels derived from the upstream schema
See docs/plugins/authoring.md.
Development
Install dependencies:
uv sync
Before calling work complete, all four local gates must pass:
uv run ruff check
uv run ruff format --check
uv run ty check
uv run pytest
The default pytest run excludes the longer fuzz suite. Run it explicitly with:
uv run pytest -m fuzz
The repo also carries integration tests that pin the important operator-facing behavior:
- exemplar end-to-end round-trip
- byte-stable idempotency across repeated merges
- login recipe docs staying aligned with the real CLI
- schema drift against vendored upstream models
CI runs the four standard gates on macOS and Linux, across Python 3.12 and 3.13.
Schema sync
Generated codec models under src/chameleon/codecs/*/_generated.py are vendored
artifacts. Do not hand-edit them.
When you intentionally refresh an upstream schema:
uv run --group schema-sync python tools/sync-schemas/sync.py claude
uv run --group schema-sync python tools/sync-schemas/sync.py codex
The Codex sync uses the pinned upstream revision from
tools/sync-schemas/pins.toml.
Status
Chameleon is pre-1.0 and still tightening parity edges, but the current shape is already practical:
- repo-backed neutral config
- bidirectional Claude/Codex reconciliation
- unattended login-time merge hooks
- interactive conflict resolution with stored decisions
- plugin target expansion path
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file chameleon_ai-0.5.11.tar.gz.
File metadata
- Download URL: chameleon_ai-0.5.11.tar.gz
- Upload date:
- Size: 553.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2fc21bd6d6f77b065dae6331b69aa07b1d92397b835db3b111fceaceb779ca88
|
|
| MD5 |
5ac4930da1327291295f2f9bc1f4b076
|
|
| BLAKE2b-256 |
a9f8765191936508317c873e0cc1aae9b53f02ccddae15cff189d0d4498895a0
|
Provenance
The following attestation bundles were made for chameleon_ai-0.5.11.tar.gz:
Publisher:
release.yaml on fnordpig/chameleon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chameleon_ai-0.5.11.tar.gz -
Subject digest:
2fc21bd6d6f77b065dae6331b69aa07b1d92397b835db3b111fceaceb779ca88 - Sigstore transparency entry: 1575459366
- Sigstore integration time:
-
Permalink:
fnordpig/chameleon@4c39b47893b500f7d9aa3a303e165133e09dc564 -
Branch / Tag:
refs/tags/v0.5.11 - Owner: https://github.com/fnordpig
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@4c39b47893b500f7d9aa3a303e165133e09dc564 -
Trigger Event:
push
-
Statement type:
File details
Details for the file chameleon_ai-0.5.11-py3-none-any.whl.
File metadata
- Download URL: chameleon_ai-0.5.11-py3-none-any.whl
- Upload date:
- Size: 193.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f51f7d26330ff4b617c2ee7c88200f8f55edda5669f3cb0c55d304f54b633bf
|
|
| MD5 |
238ea91556b7ea53d6cb73aa7e0f5513
|
|
| BLAKE2b-256 |
da9d6e3ed05f5daa5dce68d7a85540c7aa2bb2c2fda7e10946c9a159548b8f52
|
Provenance
The following attestation bundles were made for chameleon_ai-0.5.11-py3-none-any.whl:
Publisher:
release.yaml on fnordpig/chameleon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
chameleon_ai-0.5.11-py3-none-any.whl -
Subject digest:
1f51f7d26330ff4b617c2ee7c88200f8f55edda5669f3cb0c55d304f54b633bf - Sigstore transparency entry: 1575459414
- Sigstore integration time:
-
Permalink:
fnordpig/chameleon@4c39b47893b500f7d9aa3a303e165133e09dc564 -
Branch / Tag:
refs/tags/v0.5.11 - Owner: https://github.com/fnordpig
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@4c39b47893b500f7d9aa3a303e165133e09dc564 -
Trigger Event:
push
-
Statement type: