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 timeouts
  • log_level – Determines logging verbosity
  • acp_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 for lint, test, and format operations
  • ignore_patterns – File globs to exclude from processing
  • allow_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-empty
  • auto_fix – Auto-fix limits and behaviors
  • intent – Intent detection settings
  • test – 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:

  1. Initialize defaults – Start with autoFixDefaults(), intentDefaults(), and testDefaults() baseline values
  2. Apply global overrides – applyAutoFixOverrides, applyIntentOverrides, and applyTestOverrides overlay non-nil fields from the global config
  3. Apply repo overrides – The same helper functions process the repo config, allowing repository values to win
  4. Assemble the Config struct – Most fields (agent, ACP settings, CI timeout, log level) transfer directly from the global config
  5. Copy repo-only fields – Commands and IgnorePatterns transfer verbatim from the repo config
  6. Resolve agent precedence – If repo.Agent is 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 the GlobalConfig struct
  • Repo configuration (.no-mistakes.yaml) enables per-repository customization of commands, ignore patterns, and agent overrides via the RepoConfig struct
  • 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 unless allow_repo_commands is explicitly enabled
  • Key files implementing this logic reside in internal/config/config.go with 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →