# How Hister Loads Its Configuration and Where Config Files Are Located

> Discover how Hister loads configuration, its search paths, YAML parsing with Viper, and environment variable merging. Learn where Hister config files are located and how runtime values are initialized.

- Repository: [Adam Tauber/hister](https://github.com/asciimoo/hister)
- Tags: internals
- Published: 2026-09-01

---

**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`](https://github.com/asciimoo/hister/blob/main/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`](https://github.com/asciimoo/hister/blob/main/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:

```go
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:

```go
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:

```go
// 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:

```go
// 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`](https://github.com/asciimoo/hister/blob/main/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`](https://github.com/asciimoo/hister/blob/main/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`](https://github.com/asciimoo/hister/blob/main/config/config.go) will read the specified file directly, bypassing the automatic search logic defined in `getConfigSearchPaths`.