Severity: HIGH — inverted config precedence plus a global-config corruption bug that is reproducible today.
Evidence
1. internal/config/config.go:58-64 — project .cu.yml merges via viper.Set(), viper's OVERRIDE slot:
// Read project config
if err := projectViper.ReadInConfig(); err == nil {
// Merge project config with main config
// Project config takes precedence
for k, v := range projectViper.AllSettings() {
viper.Set(k, v)
}
}
viper.Set writes into the override slot, which outranks everything — including environment variables and bound flags. So project config silently beats CU_OUTPUT, --output, --debug, etc. The intended precedence is flags > env > project > global; today it is effectively project > flags > env > global.
2. internal/config/config.go:80-83 — Save() writes the ENTIRE merged state back to the global file:
func Save() error {
configPath := filepath.Join(DefaultConfigDir, ConfigFileName+"."+ConfigType)
return viper.WriteConfigAs(configPath)
}
cu config set <key> <value> calls Set + Save (internal/cmd/config.go). Because Save serializes the full merged viper state, running cu config set inside any project with a .cu.yml bakes that project's values (and all defaults) into ~/.config/cu/config.yaml. Reproducible today:
cd <repo-with-.cu.yml> # e.g. default_list: "Project X Backlog"
cu config set output json # innocuous global tweak
cat ~/.config/cu/config.yaml # now contains default_list: "Project X Backlog" globally
3. internal/cmd/root.go:91 — . on the config search path:
A config-injection hole: any directory you run cu in can supply the config file. Removal is already planned; folding it into this fix keeps the trust story coherent.
(Aside noted during review: root.go:46 help text says config.yml but Save() writes config.yaml — drive-by fix candidate.)
Fix direction
- Per-layer stores merged in code (packs → global → project), replacing the
viper.Set() merge, with documented precedence flags > env > project > global > pack defaults > built-ins.
- yaml.v3 node-based surgical writer (yaml.v3 is already a direct dependency):
cu config set updates only the touched key path in the correct file, preserving comments and ordering (the gh yamlmap approach).
Execute()-time config hoist with argv pre-scan for --config/--debug, since cobra resolves commands before OnInitialize/PersistentPreRunE run.
- Adds
cu config unset <key>, cu config list --show-origin, and --project targeting.
Why this is the keystone
This rework is item 0.2 in the context-layer design spec sequencing — the prerequisite for refs, aliases, packs, and cu onboard (all of which are config layers): see docs/design/context-layer.md §2.5 (lands via PR #39). It also independently fixes the live project→global config-bleed bug above, so it is worth doing regardless of the context layer.
Severity: HIGH — inverted config precedence plus a global-config corruption bug that is reproducible today.
Evidence
1.
internal/config/config.go:58-64— project.cu.ymlmerges viaviper.Set(), viper's OVERRIDE slot:viper.Setwrites into the override slot, which outranks everything — including environment variables and bound flags. So project config silently beatsCU_OUTPUT,--output,--debug, etc. The intended precedence isflags > env > project > global; today it is effectivelyproject > flags > env > global.2.
internal/config/config.go:80-83—Save()writes the ENTIRE merged state back to the global file:cu config set <key> <value>callsSet+Save(internal/cmd/config.go). BecauseSaveserializes the full merged viper state, runningcu config setinside any project with a.cu.ymlbakes that project's values (and all defaults) into~/.config/cu/config.yaml. Reproducible today:3.
internal/cmd/root.go:91—.on the config search path:A config-injection hole: any directory you run
cuin can supply the config file. Removal is already planned; folding it into this fix keeps the trust story coherent.(Aside noted during review:
root.go:46help text saysconfig.ymlbutSave()writesconfig.yaml— drive-by fix candidate.)Fix direction
viper.Set()merge, with documented precedenceflags > env > project > global > pack defaults > built-ins.cu config setupdates only the touched key path in the correct file, preserving comments and ordering (the gh yamlmap approach).Execute()-time config hoist with argv pre-scan for--config/--debug, since cobra resolves commands beforeOnInitialize/PersistentPreRunErun.cu config unset <key>,cu config list --show-origin, and--projecttargeting.Why this is the keystone
This rework is item 0.2 in the context-layer design spec sequencing — the prerequisite for refs, aliases, packs, and
cu onboard(all of which are config layers): seedocs/design/context-layer.md§2.5 (lands via PR #39). It also independently fixes the live project→global config-bleed bug above, so it is worth doing regardless of the context layer.