@@ -27,24 +27,38 @@ func Initialize() error {
2727 // Set config type to yaml (we only load config.yaml, not config.json)
2828 v .SetConfigType ("yaml" )
2929
30- // Explicitly locate config.yaml and use SetConfigFile to avoid picking up config.json
31- // Precedence: BEADS_DIR > project .beads/config.yaml > ~/.config/bd/config.yaml > ~/.beads/config.yaml
32- configFileSet := false
33-
34- // 0. Check BEADS_DIR first (highest priority)
35- // This ensures bd commands with BEADS_DIR set find the correct config
36- if beadsDir := os .Getenv ("BEADS_DIR" ); beadsDir != "" && ! configFileSet {
37- configPath := filepath .Join (beadsDir , "config.yaml" )
38- if _ , err := os .Stat (configPath ); err == nil {
39- v .SetConfigFile (configPath )
40- configFileSet = true
30+ // Collect config files from lowest to highest priority.
31+ // We load the lowest first with ReadInConfig, then MergeInConfig each
32+ // subsequent file so higher-priority values overwrite lower-priority ones.
33+ //
34+ // Precedence (highest to lowest):
35+ // BEADS_DIR/config.yaml > project .beads/config.yaml > ~/.config/bd/config.yaml > ~/.beads/config.yaml
36+ //
37+ // Previously, only ONE config file was loaded (the highest-priority match),
38+ // which meant user-level config was silently ignored when project-level
39+ // config existed — e.g., the idle-monitor daemon with BEADS_DIR set (GH#2375).
40+ var configPaths []string // ordered lowest priority first
41+ var primaryConfigPath string // project-level config (for config.local.yaml and SaveConfigValue)
42+
43+ // 3. Legacy: ~/.beads/config.yaml (lowest priority)
44+ if homeDir , err := os .UserHomeDir (); err == nil {
45+ p := filepath .Join (homeDir , ".beads" , "config.yaml" )
46+ if _ , err := os .Stat (p ); err == nil {
47+ configPaths = append (configPaths , p )
4148 }
4249 }
4350
44- // 1. Walk up from CWD to find project .beads/config.yaml
45- // This allows commands to work from subdirectories
51+ // 2. User: ~/.config/bd/config.yaml
52+ if configDir , err := os .UserConfigDir (); err == nil {
53+ p := filepath .Join (configDir , "bd" , "config.yaml" )
54+ if _ , err := os .Stat (p ); err == nil {
55+ configPaths = append (configPaths , p )
56+ }
57+ }
58+
59+ // 1. Project: walk up from CWD to find .beads/config.yaml
4660 cwd , err := os .Getwd ()
47- if err == nil && ! configFileSet {
61+ if err == nil {
4862 // In the beads repo, `.beads/config.yaml` is tracked and may set sync.mode=dolt-native.
4963 // In `go test` (especially for `cmd/bd`), we want to avoid unintentionally picking up
5064 // the repo-local config, while still allowing tests to load config.yaml from temp repos.
@@ -66,42 +80,31 @@ func Initialize() error {
6680 // Walk up parent directories to find .beads/config.yaml
6781 for dir := cwd ; dir != filepath .Dir (dir ); dir = filepath .Dir (dir ) {
6882 beadsDir := filepath .Join (dir , ".beads" )
69- configPath := filepath .Join (beadsDir , "config.yaml" )
70- if _ , err := os .Stat (configPath ); err == nil {
83+ p := filepath .Join (beadsDir , "config.yaml" )
84+ if _ , err := os .Stat (p ); err == nil {
7185 if ignoreRepoConfig && moduleRoot != "" {
7286 // Only ignore the repo-local config (moduleRoot/.beads/config.yaml).
73- wantIgnore := filepath .Clean (configPath ) == filepath .Clean (filepath .Join (moduleRoot , ".beads" , "config.yaml" ))
87+ wantIgnore := filepath .Clean (p ) == filepath .Clean (filepath .Join (moduleRoot , ".beads" , "config.yaml" ))
7488 if wantIgnore {
7589 continue
7690 }
7791 }
78- // Found .beads/config.yaml - set it explicitly
79- v .SetConfigFile (configPath )
80- configFileSet = true
92+ configPaths = append (configPaths , p )
93+ primaryConfigPath = p
8194 break
8295 }
8396 }
8497 }
8598
86- // 2. User config directory (~/.config/bd/config.yaml)
87- if ! configFileSet {
88- if configDir , err := os .UserConfigDir (); err == nil {
89- configPath := filepath .Join (configDir , "bd" , "config.yaml" )
90- if _ , err := os .Stat (configPath ); err == nil {
91- v .SetConfigFile (configPath )
92- configFileSet = true
93- }
94- }
95- }
96-
97- // 3. Home directory (~/.beads/config.yaml)
98- if ! configFileSet {
99- if homeDir , err := os .UserHomeDir (); err == nil {
100- configPath := filepath .Join (homeDir , ".beads" , "config.yaml" )
101- if _ , err := os .Stat (configPath ); err == nil {
102- v .SetConfigFile (configPath )
103- configFileSet = true
99+ // 0. BEADS_DIR: highest priority
100+ if beadsDir := os .Getenv ("BEADS_DIR" ); beadsDir != "" {
101+ p := filepath .Join (beadsDir , "config.yaml" )
102+ if _ , err := os .Stat (p ); err == nil {
103+ // Avoid duplicate if BEADS_DIR points to same config as CWD walk
104+ if primaryConfigPath == "" || filepath .Clean (p ) != filepath .Clean (primaryConfigPath ) {
105+ configPaths = append (configPaths , p )
104106 }
107+ primaryConfigPath = p
105108 }
106109 }
107110
@@ -194,23 +197,37 @@ func Initialize() error {
194197 // Maps project names to paths for resolving external: blocked_by references
195198 v .SetDefault ("external_projects" , map [string ]string {})
196199
197- // Read config file if it was found
198- if configFileSet {
200+ // Load config files: lowest priority first, each MergeInConfig overwrites
201+ if len (configPaths ) > 0 {
202+ v .SetConfigFile (configPaths [0 ])
199203 if err := v .ReadInConfig (); err != nil {
200204 return fmt .Errorf ("error reading config file: %w" , err )
201205 }
202- debug .Logf ("Debug: loaded config from %s\n " , v .ConfigFileUsed ())
206+ debug .Logf ("Debug: loaded config from %s\n " , configPaths [0 ])
207+
208+ for _ , p := range configPaths [1 :] {
209+ v .SetConfigFile (p )
210+ if err := v .MergeInConfig (); err != nil {
211+ return fmt .Errorf ("error merging config file %s: %w" , p , err )
212+ }
213+ debug .Logf ("Debug: merged config from %s\n " , p )
214+ }
215+
216+ // Restore primary config path as ConfigFileUsed (used by SaveConfigValue,
217+ // ResolveExternalProjectPath, etc.)
218+ v .SetConfigFile (primaryConfigPath )
203219
204220 // Merge local config overrides if present (config.local.yaml)
205221 // This allows machine-specific settings without polluting tracked config
206- configDir := filepath .Dir (v .ConfigFileUsed ())
207- localConfigPath := filepath .Join (configDir , "config.local.yaml" )
222+ localConfigPath := filepath .Join (filepath .Dir (primaryConfigPath ), "config.local.yaml" )
208223 if _ , err := os .Stat (localConfigPath ); err == nil {
209224 v .SetConfigFile (localConfigPath )
210225 if err := v .MergeInConfig (); err != nil {
211226 return fmt .Errorf ("error merging local config file: %w" , err )
212227 }
213228 debug .Logf ("Debug: merged local config from %s\n " , localConfigPath )
229+ // Restore primary as ConfigFileUsed
230+ v .SetConfigFile (primaryConfigPath )
214231 }
215232 } else {
216233 // No config.yaml found - use defaults and environment variables
0 commit comments