Project Config Arrays Should Replace, Not Merge
Small merge rules create big behavior differences.
One of the most important ones in APC and APX is this: when a project overrides an array in .apc/config.json, that array should replace the global one, not merge into it.
APC is the portable context layer. It keeps the repository-owned contract in files like AGENTS.md, .apc/project.json, and .apc/config.json. APX is the daily-use runtime and tooling layer. It reads that contract, combines it with machine-local settings from ~/.apx/config.json, and runs the effective result in the CLI, daemon, web admin, and other surfaces.
That means merge behavior is not a side detail. It decides what the runtime actually does.
The APX implementation states the rule directly in src/host/daemon/project-config.js: project config deep-merges over global config, objects recurse, primitives override, and arrays in the project config replace arrays from the global layer wholesale.
That last part is the right choice.
Why merging arrays creates bad surprises
Object merges are usually safe because keys have names. Arrays are different. Position matters, order matters, and combining entries often changes meaning.
Imagine a machine-local global config like this:
{
"telegram": {
"channels": [
{ "name": "default", "chat_id": "111" },
{ "name": "ops", "chat_id": "222" }
]
},
"super_agent": {
"allowed_tools": ["read_file", "list_files"]
}
}
Now imagine a repository wants a stricter project contract:
{
"telegram": {
"channels": [
{ "name": "review", "chat_id": "333", "route_to_agent": "reviewer" }
]
},
"super_agent": {
"allowed_tools": ["read_file"]
}
}
If APX merged those arrays item-by-item, the project would inherit channels and tool allowances it did not ask for. The effective config would look bigger than the repository contract, and nobody reviewing .apc/config.json could trust that file to describe actual project behavior.
Replacement avoids that ambiguity.
With replacement semantics, the project says exactly what it means:
- these are the channels for this project
- this is the tool allowlist for this project
- this is the fallback model order for this project
No hidden append. No accidental leftovers from one laptop.
Why this fits APC and APX
This rule only makes sense if APC and APX have different jobs.
APC should carry portable project intent. That means committed config must stay reviewable and predictable. When a team reads .apc/config.json, they should be able to reason about the whole project override without guessing how an array was stitched together from some machine-local base.
APX should do the runtime work. It should compute the effective config for a given project and apply it locally. That is exactly what APX does through its daemon-side project config loader and commands like:
apx project config show my-app --effective
apx project config set my-app super_agent.permission_mode total
apx project config set my-app telegram.route_to_agent reviewer
So APC owns the project override. APX owns the merge engine. The clean contract is: objects merge, arrays replace.
Practical benefit: fewer accidental policies
The biggest advantage is not elegance. It is operational safety.
Array settings usually define policy sets, ordered routes, or explicit lists. Those are the worst places for accidental inheritance.
A project should not silently inherit:
- extra Telegram channels
- extra allowed tools
- extra fallback models
- extra routines
from a developer's global machine config just because both layers happened to use arrays.
Replacement makes project overrides boring, and boring is correct here.
If a repository wants one entry, it gets one entry. If it wants three, it writes three. Reviewers can inspect the file and know what APX will serve for that project.
That is the bigger APC/APX pattern in miniature.
APC keeps the shared contract visible.
APX makes that contract executable without leaking machine-local noise back into the repo.
A good portable context layer should not depend on clever merge magic.
It should depend on rules a team can read and trust.
For project arrays, replacement is that rule.
Top comments (0)