Skip to content

config: project .cu.yml merges via viper.Set and Save() writes merged state back to global config #37

Description

@timimsms

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-83Save() 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:

viper.AddConfigPath(".")

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions