DEV Community

Cover image for Project Config Arrays Should Replace, Not Merge

Project Config Arrays Should Replace, Not Merge

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"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)