Global vs Repo Config in no-mistakes: How Configuration Merging Works
no-mistakes distinguishes global user-wide defaults from repository-specific overrides, merging them through a hierarchical overlay where repo configurations take precedence over global settings while maintaining security through trusted-source validation.
The no-mistakes tool uses a two-tier configuration system to balance user preferences with project-specific requirements. Understanding how global and repo config in no-mistakes interact ensures you can set organization-wide defaults while allowing individual repositories to customize behavior safely. Both configurations are YAML files parsed into Go structs and combined into a single runtime Config that drives the pipeline.
Where Global and Repo Configurations Live
Global Configuration Path and Structure
The global configuration resides at ~/.no-mistakes/config.yaml and is created automatically by EnsureDefaultGlobalConfig. This file holds defaults that apply to all repositories a user works with, including default agent settings, auto-fix limits, log levels, and CI timeouts.
In internal/config/config.go, the GlobalConfig struct is defined around lines 40-52, containing fields such as agent, acp_path, ci_timeout, and log_level.
Repo Configuration Path and Structure
The repo configuration lives at <repo-root>/.no-mistakes.yaml and uses the RepoConfig struct defined around lines 69-84 in the same file. This scope allows repositories to customize behavior through custom commands, ignore patterns, per-repo agents, and fine-grained auto-fix settings.
What Each Configuration Scope Controls
Global-Only Settings
Certain fields exist only in the global scope and affect daemon behavior across all projects:
ci_timeout– Controls CI operation timeoutslog_level– Determines logging verbosityacp_path/acp_registry_overrides/agent_path_override/agent_args_override– Global defaults for ACP agents and native-agent binary overrides
Repo-Only Settings
These fields are exclusive to repository configurations and enable project-specific customization:
commands– Explicit shell commands forlint,test, andformatoperationsignore_patterns– File globs to exclude from processingallow_repo_commands– Security flag determining whether to trust the pushed copy or default-branch copy of the config for code-executing fields
Overridable Settings
Both scopes can declare these values, with repo configurations taking precedence when non-nil:
agent– Repository agent overrides global default when non-emptyauto_fix– Auto-fix limits and behaviorsintent– Intent detection settingstest– Test execution parameters
Loading Configurations from Disk
The internal/config/config.go file provides distinct loader functions for each scope. LoadGlobal begins at line 66 and parses the YAML from the user's home directory, falling back to defaults if the file is missing. LoadRepo starts at line 50 and loads the configuration from the repository root, returning an empty struct if no file exists.
// Load global config with fallback defaults
global, err := config.LoadGlobal(filepath.Join(os.Getenv("HOME"), ".no-mistakes", "config.yaml"))
if err != nil {
log.Fatal(err)
}
// Load repo config from worktree root
repo, err := config.LoadRepo(repoRootPath)
if err != nil {
log.Fatal(err)
}
The Merge Algorithm: How Global and Repo Config Combine
The central Merge(global, repo) *Config function, implemented in lines 50-85 of internal/config/config.go, implements a hierarchical overlay strategy:
- Initialize defaults – Start with
autoFixDefaults(),intentDefaults(), andtestDefaults()baseline values - Apply global overrides –
applyAutoFixOverrides,applyIntentOverrides, andapplyTestOverridesoverlay non-nil fields from the global config - Apply repo overrides – The same helper functions process the repo config, allowing repository values to win
- Assemble the Config struct – Most fields (agent, ACP settings, CI timeout, log level) transfer directly from the global config
- Copy repo-only fields –
CommandsandIgnorePatternstransfer verbatim from the repo config - Resolve agent precedence – If
repo.Agentis non-empty, it replaces the global-derived agent (lines 80-82)
The function returns a unified *Config pointer that the pipeline uses for execution.
Security: Trusted vs Pushed Configuration
The EffectiveRepoConfig function (lines 86-102) implements a security boundary for code-executing fields. By default, the daemon only trusts the default-branch copy of .no-mistakes.yaml rather than the pushed copy from contributors. This prevents supply-chain attacks where malicious shell commands or agent swaps could be injected.
When allow_repo_commands is false (the default), the effective configuration draws Commands and Agent exclusively from the trusted default-branch copy, ignoring potentially malicious values from pull requests.
trusted, _ := config.LoadRepoFromBytes(trustedYAML) // default-branch copy
pushed, _ := config.LoadRepoFromBytes(pushedYAML) // contributor copy
effective := config.EffectiveRepoConfig(pushed, trusted, false)
// effective.Commands and effective.Agent now come only from trusted source
Working with Configurations in Code
Loading and Merging Configurations
package main
import (
"fmt"
"os"
"path/filepath"
"github.com/kunchenguid/no-mistakes/internal/config"
)
func main() {
// Load global configuration
globalPath := filepath.Join(os.Getenv("HOME"), ".no-mistakes", "config.yaml")
global, err := config.LoadGlobal(globalPath)
if err != nil {
panic(err)
}
// Load repository configuration
repoRoot, _ := os.Getwd()
repo, err := config.LoadRepo(repoRoot)
if err != nil {
panic(err)
}
// Merge into final runtime configuration
finalCfg := config.Merge(global, repo)
fmt.Printf("Effective agent: %s\n", finalCfg.Agent)
fmt.Printf("Auto-fix lint limit: %d\n", finalCfg.AutoFix.Lint)
fmt.Printf("Repo commands: %s %s %s\n",
finalCfg.Commands.Lint, finalCfg.Commands.Test, finalCfg.Commands.Format)
}
Validating Security-Effective Configuration
trusted, _ := config.LoadRepoFromBytes(trustedYAML)
pushed, _ := config.LoadRepoFromBytes(pushedYAML)
effective := config.EffectiveRepoConfig(pushed, trusted, false)
fmt.Printf("Effective agent: %s\n", effective.Agent)
fmt.Printf("Effective lint command: %s\n", effective.Commands.Lint)
Summary
- Global configuration (
~/.no-mistakes/config.yaml) provides user-wide defaults for agents, timeouts, and logging via theGlobalConfigstruct - Repo configuration (
.no-mistakes.yaml) enables per-repository customization of commands, ignore patterns, and agent overrides via theRepoConfigstruct - The merge process initializes defaults, overlays global values, then overlays repo values, with repo configurations taking precedence for non-nil fields
- Security protection ensures that code-executing fields (
commands,agent) come only from the trusted default-branch copy unlessallow_repo_commandsis explicitly enabled - Key files implementing this logic reside in
internal/config/config.gowith specific functions at lines 50 (LoadRepo), 66 (LoadGlobal), and 50-85 (Merge)
Frequently Asked Questions
What takes precedence when the same setting exists in both global and repo config?
Repository configurations take precedence over global settings when the repo value is non-nil. The Merge function applies global overrides first, then overlays repo values, ensuring that repository-specific intents, auto-fix limits, and agent settings silently win while shared defaults remain consistent.
Can repository configurations execute arbitrary shell commands?
Yes, but only from trusted sources. The commands field (specifying lint, test, and format shells) is repo-only and can execute arbitrary commands. However, by default, no-mistakes only uses the default-branch copy of .no-mistakes.yaml for these fields, not the pushed copy from contributors, unless the maintainer explicitly sets allow_repo_commands to true.
Where does no-mistakes store global configuration files?
The global configuration stores at ~/.no-mistakes/config.yaml in the user's home directory. The LoadGlobal function in internal/config/config.go (line 66) reads this path, and the tool creates it automatically via EnsureDefaultGlobalConfig if missing.
How does the merge algorithm handle nil or empty values?
The merge algorithm treats nil values as "not specified" and preserves existing values from previous layers. During the overlay process (lines 50-85), helper functions like applyAutoFixOverrides only replace values when the source configuration provides non-nil fields, ensuring that empty repo settings do not accidentally clear valid global defaults.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →