How Hister Loads Its Configuration and Where Config Files Are Located

Hister loads its configuration in three distinct stages—locating the file through OS-specific search paths, parsing YAML with Viper while merging environment variables, and initializing runtime values like data directories and secret keys.

Hister (asciimoo/hister) is a self-hosted bookmark and reading list manager written in Go. Understanding how Hister configuration is loaded and where the system expects to find config files is essential for proper deployment and customization. The configuration system supports explicit file paths, conventional OS directories, environment variable overrides, and legacy fallback locations.

How Hister Configuration Loading Works

The configuration pipeline is implemented in config/config.go and proceeds through three distinct phases: file discovery, YAML parsing with environment merging, and runtime initialization.

Stage 1: Locating the Configuration File

The entry point readConfigFile (lines 62‑90) checks for an explicitly provided path first. If the user passes --config <path>, that file is read directly. When no path is specified, the function calls getConfigSearchPaths (lines 94‑52) to generate an ordered list of conventional locations based on the operating system.

The first existing file in the returned list is selected. If the deprecated ~/.histerrc is found, Hister logs a warning but continues loading.

Stage 2: Parsing with Viper

Once file bytes are obtained, parseConfig (lines 107‑26) orchestrates the parsing:

  1. loadViper (lines 54‑66) creates a Viper instance, sets the config type to YAML, and reads the file content.
  2. bindEnvironment (lines 69‑90) automatically binds environment variables prefixed with HISTER__, mapping double underscores to nested keys (e.g., HISTER__APP__LOG_LEVEL becomes app.log_level).
  3. The data is unmarshaled onto a Config struct pre-populated with sensible defaults via CreateDefaultConfig.

Stage 3: Runtime Initialization

After unmarshaling, Config.init (lines 37‑53) prepares the runtime environment:

  • Path expansion: Resolves ~/ shortcuts to the user's home directory.
  • Data directory: Creates the configured data directory (defaulting to platform-specific locations via getDefaultDataDir), falling back to /var/lib/hister if creation fails.
  • Secret generation: Persists a random secret key to <data-dir>/.secret_key if the file does not exist.
  • TUI loading: Invokes LoadTUIConfig to read tui.yaml from the same directory as the main config, creating it with defaults if absent.
  • Validation: Verifies hotkey definitions, OAuth provider settings, and semantic search configuration.

Hister Config File Locations by Operating System

When no explicit path is provided, Hister searches the following locations in order, stopping at the first existing file:

  • macOS:

    • ~/Library/Preferences/hister/config.yml
    • ~/Library/Application Support/hister/config.yml
    • Legacy ~/.histerrc
    • ~/.config/hister/config.yml
  • Windows:

    • %LOCALAPPDATA%\hister\config.yml
    • %XDG_CONFIG_HOME%\hister\config.yml (if set)
    • %APPDATA%\hister\config.yml
    • Legacy ~/.histerrc
    • ~/.config/hister/config.yml
  • Linux / Other Unix:

    • $XDG_CONFIG_HOME/hister/config.yml (if set)
    • ~/.config/hister/config.yml
    • Legacy ~/.histerrc

The search order ensures that platform-specific conventions take precedence, with XDG Base Directory specification respected on Unix systems.

Working with Hister Configuration Programmatically

The config package exposes functions to load and inspect configuration values from Go code.

Loading from a Custom Path

To load a specific configuration file directly:

cfg, err := config.Load("/etc/hister/custom.yml")
if err != nil {
    log.Fatal(err)
}
fmt.Println("Base URL:", cfg.Server.BaseURL)

The Load function (lines 20‑34) reads the specified file and returns a fully initialized *Config struct.

Using Default Search Paths

Pass an empty string to trigger the automatic search behavior:

cfg, err := config.Load("")
if err != nil {
    log.Fatalf("cannot load config: %v", err)
}
fmt.Printf("Using config file: %s\n", cfg.Filename())

When empty, readConfigFile traverses the OS-specific locations described above until it finds a valid file.

Resolving Derived Paths

The FullPath method (lines 53‑65) resolves filenames relative to the configured data directory or executable location:

// Returns absolute path like "/home/user/.config/hister/db.sqlite3"
dbPath := cfg.FullPath(cfg.Server.Database)

Accessing TUI Configuration

After initialization, the TUI configuration is available through the main config object:

// After the main config is loaded, the TUI config lives beside the main file.
fmt.Println("TUI dark theme:", cfg.TUI.DarkTheme)

The LoadTUIConfig function reads tui.yaml from the same directory as the main configuration file and is invoked automatically during Config.init.

Environment Variable Override

Hister supports complete configuration via environment variables using a hierarchical naming convention. The bindEnvironment function (lines 69‑90) maps variables prefixed with HISTER__ to nested configuration keys, using double underscores as delimiters.

For example:

  • HISTER__APP__LOG_LEVEL maps to app.log_level
  • HISTER__SERVER__PORT maps to server.port
  • HISTER__DATABASE__PATH maps to database.path

These values override settings from the YAML file during the loadViper phase.

Summary

  • Hister configuration loading proceeds in three stages: file location (via readConfigFile and getConfigSearchPaths), parsing (via loadViper and parseConfig), and runtime initialization (via Config.init).
  • Config files are searched in OS-specific paths following platform conventions, with legacy support for ~/.histerrc.
  • Environment variables prefixed with HISTER__ override file settings using double-underscore notation to represent nested keys.
  • Runtime initialization automatically creates data directories, generates secret keys, and loads companion tui.yaml configurations.

Frequently Asked Questions

Where does Hister store its configuration file on Linux?

On Linux and other Unix systems, Hister checks $XDG_CONFIG_HOME/hister/config.yml first, defaulting to ~/.config/hister/config.yml if the environment variable is unset. It also maintains legacy support for ~/.histerrc as a final fallback, though this location is deprecated.

Can I use environment variables instead of a config file?

Yes. Hister automatically binds environment variables prefixed with HISTER__ to configuration keys. Use double underscores to denote nesting, such as HISTER__SERVER__BASE_URL for the server.base_url setting. These values take precedence over YAML file contents during the merge phase implemented in bindEnvironment.

What happens if no configuration file exists?

If no file is found in the search paths, Hister initializes a configuration object using CreateDefaultConfig to populate sensible defaults. The Config.init method then creates the necessary data directory structure, generates a secret key, and prepares runtime values, allowing the application to start with built-in defaults.

How do I specify a custom configuration file location?

Pass the file path using the --config command-line flag. The Load function in config/config.go will read the specified file directly, bypassing the automatic search logic defined in getConfigSearchPaths.

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 →