Skill: samber/golang-spf13-viper (version 1.0.1)

--- name: golang-spf13-viper description: "Golang configuration library using spf13/viper — layered precedence (flag > env > file > KV > default), BindPFlag/BindPFlags, SetEnvPrefix + SetEnvKeyReplacer + AutomaticEnv, ReadInConfig + ConfigFileNotFoundError, Unmarshal + mapstructure struct tags, Sub for sub-trees, WatchConfig + OnConfigChange for hot reload, viper.New() for test isolation, and remote KV integration. Apply when using or adopting spf13/viper, or when the codebase imports `github.com/spf13/viper`. For CLI command structure alongside viper, see the `samber/cc-skills-golang@golang-spf13-cobra` skill. For general CLI architecture, see `samber/cc-skills-golang@golang-cli`." user-invocable: true license: MIT compatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang. metadata: author: samber version: "1.0.1" openclaw: emoji: "🔧" homepage: https://github.com/samber/cc-skills-golang requires: bins: - go install: [] skill-library-version: "1.21.0" allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent WebFetch mcp__context7__resolve-library-id mcp__context7__query-docs --- **Persona:** You are a Go engineer who treats configuration as a layered system. Flag beats env beats file beats default — and you bind every key so all four layers stay reachable through one API. # Using spf13/viper for layered configuration in Go Viper resolves configuration values from multiple sources in a fixed precedence order. It has no user-facing surface — it doesn't define commands or flags. Its job is to answer "what is the value of key X right now?" by walking its source layers from highest to lowest priority. **Official Resources:** - [pkg.go.dev/github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper) - [github.com/spf13/viper](https://github.com/spf13/viper) This skill is not exhaustive. Please refer to library documentation and code examples for more information. Context7 can help as a discoverability platform. ```bash go get github.com/spf13/viper@latest ``` ## Viper vs. cobra Cobra owns the command tree — subcommands, flags, arg validation, completions. Viper owns configuration resolution — it answers "what is the value of key X?" by walking its source layers. Viper has no user-facing surface; it is purely a key-value resolver. Use cobra alone for flag-only CLIs; viper alone for config-file daemons; both when you need both, binding flags at `PersistentPreRunE` via `BindPFlag`. → See `samber/cc-skills-golang@golang-spf13-cobra` for the cobra side of this integration. ## The precedence pipeline Viper resolves a key by walking sources in this order (first set value wins): ``` 1. explicit Set() — viper.Set("key", val) highest priority 2. flag — bound pflag.Flag 3. env var — BindEnv / AutomaticEnv 4. config file — ReadInConfig / MergeInConfig 5. KV remote — etcd / Consul 6. default — viper.SetDefault("key", val) lowest priority ``` This pipeline is fixed and cannot be reordered. Understanding it prevents most viper bugs: a key that "should" come from a config file may be shadowed by an env var or a flag with a default value. ## Sources and config files ```go viper.SetConfigName("config") viper.AddConfigPath("$HOME/.myapp") if err := viper.ReadInConfig(); err != nil { var notFound *viper.ConfigFileNotFoundError if !errors.As(err, &notFound) { return fmt.Errorf("reading config: %w", err) // propagate real errors only } } ``` `ConfigFileNotFoundError` must be handled gracefully — config files are usually optional. An unhandled error from a missing file crashes programs that are perfectly valid when run with only flags or env vars. For supported formats (JSON, TOML, YAML, HCL, INI, properties), `MergeInConfig`, and remote KV, see [sources-and-formats.md](references/sources-and-formats.md). ## Env binding and key replacers This is the highest-bug-density area in viper. All three settings must be wired together — missing any one breaks nested key resolution: ```go // ✓ Good — all three wired together at startup viper.SetEnvPrefix("MYAPP") // prevent collisions: PORT → MYAPP_PORT viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // database.host → MYAPP_DATABASE_HOST viper.AutomaticEnv() // ✗ Bad — without SetEnvKeyReplacer, viper looks for MYAPP_DATABASE.HOST (dot preserved) ``` For `BindEnv`, `AllowEmptyEnv`, and env-vs-default interaction, see [binding-and-env.md](references/binding-and-env.md). ## Flag binding (the cobra seam) Bind cobra flags to viper in `init()` or `PersistentPreRunE` — never in `RunE` (config loading in `PersistentPreRunE` already ran before `RunE`, so bindings set in `RunE` are missed): ```go func init() { rootCmd.PersistentFlags().Int("port", 8080, "listen port") viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port")) // viper.BindPFlags(cmd.Flags()) — bind an entire FlagSet at once } ``` For `AllowEmptyEnv` and flag/env interaction details, see [binding-and-env.md](references/binding-and-env.md). ## Unmarshaling into structs `viper.Unmarshal` maps the resolved configuration into a struct using `mapstructure`: ```go type Config struct { Port int `mapstructure:"port"` Database struct { MaxConn int `mapstructure:"max_conn"` // explicit tag: mapstructure won't convert underscore→camelCase } `mapstructure:"database"` } var cfg Config viper.Unmarshal(&cfg) ``` **Always use `mapstructure` tags** — implicit mapping is fragile for nested structs and underscore-named fields. Prefer `UnmarshalKey("database", &dbCfg)` over `Sub("database").Unmarshal` — it avoids the nil-check `Sub` requires when the key is missing. For `time.Duration` / `net.IP` / slice decoders and custom `DecodeHook` registration, see [unmarshal.md](references/unmarshal.md). ## Sub-trees `viper.Sub("database")` returns a new `*viper.Viper` scoped to the prefix, or **nil** if the key does not exist — always nil-check before calling methods on the result. Prefer `UnmarshalKey("database", &dbCfg)` which avoids the nil risk entirely. ## Hot reload ```go viper.WatchConfig() viper.OnConfigChange(func(e fsnotify.Event) { /* re-apply changed values */ }) ``` `WatchConfig` uses fsnotify and watches inodes. Editors that write atomically via rename (vim, neovim) replace the inode — the callback may not fire. Test hot-reload with `echo >> config.yaml`, not editor saves. For race-safe reload patterns, see [watch-and-reload.md](references/watch-and-reload.md). ## Test isolation **Never use the global viper in tests** — state leaks across test cases. Use `viper.New()` per test so each instance is isolated: ```go v := viper.New() v.SetConfigFile("testdata/config.yaml") require.NoError(t, v.ReadInConfig()) ``` For `t.Setenv` interactions and `Reset()` limitations, see [testing-and-isolation.md](references/testing-and-isolation.md). ## Best Practices 1. **Set prefix + key replacer + AutomaticEnv together** — missing any one causes nested env keys to silently not resolve (`database.host` → `DATABASE.HOST` instead of `DATABASE_HOST`). 2. **Handle `ConfigFileNotFoundError` gracefully** — a missing config file should not crash a service that runs with only flags and env vars. 3. **Always use `mapstructure` tags on config structs** — implicit mapping silently misses nested and underscore-named fields. 4. **Use `viper.New()` in tests, never the global** — the global accumulates state across test runs; per-test instances are isolated. 5. **Bind flags before `Execute()`** — binding in `RunE` is too late; cobra parses flags before `RunE` runs. ## Common Mistakes | Mistake | Why it fails | Fix | | --- | --- | --- | | `AutomaticEnv` without `SetEnvKeyReplacer` | `database.host` looks for `MYAPP_DATABASE.HOST` (dot preserved) — never matches | Add `SetEnvKeyReplacer(strings.NewReplacer(".", "_"))` before `AutomaticEnv` | | No `mapstructure` tags on struct fields | Silently misses nested and underscore-named fields | Add `mapstructure:"key_name"` to every field | | Using global viper in tests | State from one test contaminates the next, causing flaky ordering | Create `viper.New()` per test | | Missing `ConfigFileNotFoundError` check | Missing config file crashes a service that should run on flags/env alone | `errors.As(err, &notFound)` — only propagate non-not-found errors | ## Further Reading - [sources-and-formats.md](references/sources-and-formats.md) — supported file formats, multi-path search, MergeInConfig, remote KV (etcd/Consul) - [binding-and-env.md](references/binding-and-env.md) — BindEnv, AutomaticEnv, SetEnvPrefix, SetEnvKeyReplacer, AllowEmptyEnv, timing rules - [unmarshal.md](references/unmarshal.md) — Unmarshal, UnmarshalKey, mapstructure tags, custom DecodeHooks (Duration, IP, slice) - [watch-and-reload.md](references/watch-and-reload.md) — WatchConfig, OnConfigChange, fsnotify caveats, atomic-rename trap, race-safe patterns - [testing-and-isolation.md](references/testing-and-isolation.md) — viper.New() per test, t.Setenv interactions, Reset() limitations, snapshot/restore ## Cross-References - → See `samber/cc-skills-golang@golang-cli` skill for general CLI architecture — project layout, exit codes, signal handling, cobra+viper integration - → See `samber/cc-skills-golang@golang-spf13-cobra` skill for the cobra side of this integration (flag definition and binding) - → See `samber/cc-skills-golang@golang-testing` skill for general Go testing patterns If you encounter a bug or unexpected behavior in spf13/viper, open an issue at <https://github.com/spf13/viper/issues>.

path: evals/evals.json
content: [
  {
    "id": 1,
    "name": "env-key-replacer-nested-keys",
    "description": "Tests SetEnvKeyReplacer for nested keys with dots mapping to underscore env vars",
    "prompt": "I'm using viper in my Go service. I have a config key 'database.host' that should be configurable via an env var. I've set AutomaticEnv() and SetEnvPrefix('APP'). My env var APP_DATABASE_HOST is set but viper returns the default. What's wrong?",
    "trap": "Without the skill, the model may suggest checking the env var name, case sensitivity, or re-reading the config. The root cause is the missing SetEnvKeyReplacer — viper looks for APP_DATABASE.HOST (dot preserved), not APP_DATABASE_HOST.",
    "assertions": [
      {
        "id": "1.1",
        "text": "Identifies the root cause as missing SetEnvKeyReplacer"
      },
      {
        "id": "1.2",
        "text": "Explains that viper preserves the dot in 'database.host' when looking up the env var"
      },
      {
        "id": "1.3",
        "text": "Provides the fix: viper.SetEnvKeyReplacer(strings.NewReplacer(\".\", \"_\"))"
      },
      {
        "id": "1.4",
        "text": "Shows that the full setup requires prefix + replacer + AutomaticEnv together"
      },
      {
        "id": "1.5",
        "text": "Does NOT suggest renaming the config key to avoid dots"
      }
    ]
  },
  {
    "id": 2,
    "name": "sub-returns-nil",
    "description": "Tests that viper.Sub() returns nil when the key doesn't exist and must be nil-checked",
    "prompt": "I'm using viper.Sub('database') in my Go service to get a sub-viper for database config, then calling sub.Unmarshal(&dbCfg). Occasionally the service panics with a nil pointer dereference. What's happening?",
    "trap": "Without the skill, the model may suggest checking the config file format or adding error handling to Unmarshal. The root cause is Sub() returning nil when the 'database' key doesn't exist, and nil.Unmarshal panics.",
    "assertions": [
      {
        "id": "2.1",
        "text": "Identifies that viper.Sub() returns nil when the key doesn't exist"
      },
      {
        "id": "2.2",
        "text": "Shows adding a nil check: if sub := viper.Sub(\"database\"); sub != nil { ... }"
      },
      {
        "id": "2.3",
        "text": "Suggests returning a clear error or using defaults when sub is nil"
      },
      {
        "id": "2.4",
        "text": "Does NOT suggest checking err from Sub() (it returns no error, only nil)"
      },
      {
        "id": "2.5",
        "text": "Optionally suggests UnmarshalKey(\"database\", &dbCfg) as an alternative that avoids Sub() entirely"
      }
    ]
  },
  {
    "id": 3,
    "name": "config-file-not-found-graceful",
    "description": "Tests graceful handling of ConfigFileNotFoundError for optional config files",
    "prompt": "My Go service uses viper to read a config file. When users run it without a config file, it crashes with 'Config File config not found in ...'. The config file should be optional. How do I fix this?",
    "trap": "Without the skill, the model may suggest pre-checking if the file exists before calling ReadInConfig, or using os.Stat. The correct pattern is errors.As with viper.ConfigFileNotFoundError.",
    "assertions": [
      {
        "id": "3.1",
        "text": "Uses errors.As(err, &notFound) with *viper.ConfigFileNotFoundError"
      },
      {
        "id": "3.2",
        "text": "Only propagates errors that are NOT ConfigFileNotFoundError"
      },
      {
        "id": "3.3",
        "text": "Continues execution normally when the config file is not found"
      },
      {
        "id": "3.4",
        "text": "Does NOT use os.Stat or file existence check as the solution"
      },
      {
        "id": "3.5",
        "text": "Does NOT ignore all errors from ReadInConfig (real errors like bad YAML should still propagate)"
      }
    ]
  },
  {
    "id": 4,
    "name": "global-viper-test-pollution",
    "description": "Tests viper.New() for test isolation instead of the global instance",
    "prompt": "My Go tests for config loading are flaky — they pass when run in isolation but fail in a certain order. Each test calls viper.SetConfigFile and viper.ReadInConfig. What's causing this and how do I fix it?",
    "trap": "Without the skill, the model may suggest adding t.Cleanup(viper.Reset) or running tests with -count=1. The correct fix is creating viper.New() per test to avoid shared global state.",
    "assertions": [
      {
        "id": "4.1",
        "text": "Identifies the root cause as shared global viper state across tests"
      },
      {
        "id": "4.2",
        "text": "Recommends viper.New() per test to create an isolated instance"
      },
      {
        "id": "4.3",
        "text": "Shows v := viper.New() and using v.SetConfigFile, v.ReadInConfig, etc."
      },
      {
        "id": "4.4",
        "text": "Does NOT recommend viper.Reset() as the primary solution"
      },
      {
        "id": "4.5",
        "text": "Explains why the tests are order-dependent (state set in one test persists to the next)"
      }
    ]
  },
  {
    "id": 5,
    "name": "unmarshal-mapstructure-tags",
    "description": "Tests use of mapstructure struct tags for correct Unmarshal behavior",
    "prompt": "I'm unmarshaling my viper config into a Go struct. My config file has 'max_conn: 25' but after calling viper.Unmarshal(&cfg), cfg.MaxConn is always 0. My struct has field MaxConn int. What's wrong?",
    "trap": "Without the skill, the model may suggest checking the config file key name or viper.GetInt. The fix is adding mapstructure:\"max_conn\" tag — without it, mapstructure uses case-insensitive name matching but doesn't handle underscores to camel-case conversion.",
    "assertions": [
      {
        "id": "5.1",
        "text": "Identifies the missing mapstructure struct tag as the root cause"
      },
      {
        "id": "5.2",
        "text": "Shows adding `mapstructure:\"max_conn\"` to the MaxConn field"
      },
      {
        "id": "5.3",
        "text": "Explains that mapstructure does case-insensitive matching but not underscore-to-camelcase conversion"
      },
      {
        "id": "5.4",
        "text": "Does NOT suggest using viper.GetInt as the fix (Unmarshal should work once tagged correctly)"
      }
    ]
  },
  {
    "id": 6,
    "name": "bind-pflag-timing",
    "description": "Tests that BindPFlag must be called before Execute() runs",
    "prompt": "I'm integrating cobra and viper in my Go CLI. I call viper.BindPFlag in the command's RunE function to bind the --port flag. But viper.GetInt('port') always returns the default 8080, even when I pass --port 9090. Why?",
    "trap": "Without the skill, the model may check the flag name spelling or viper setup. The root cause is binding after Execute() has already parsed the flags — BindPFlag must happen in init() or PersistentPreRunE, before the flags are resolved.",
    "assertions": [
      {
        "id": "6.1",
        "text": "Identifies that BindPFlag must be called before Execute() / before RunE runs"
      },
      {
        "id": "6.2",
        "text": "Recommends moving BindPFlag to init() or PersistentPreRunE"
      },
      {
        "id": "6.3",
        "text": "Explains that cobra parses flags before RunE runs — binding after parsing misses the Changed state"
      },
      {
        "id": "6.4",
        "text": "Shows the correct pattern: define flag + BindPFlag in init()"
      }
    ]
  },
  {
    "id": 7,
    "name": "viper-key-case-insensitivity",
    "description": "Tests understanding that viper keys are always lowercased internally",
    "prompt": "I'm calling viper.GetString('DATABASE_HOST') in my Go service to read a config key. The config file has 'database_host: localhost' but I get an empty string. What's wrong?",
    "trap": "Without the skill, the model may suggest checking env binding or config format. Viper lowercases all keys internally — 'DATABASE_HOST' is stored as 'database_host', but the lookup 'DATABASE_HOST' is also lowercased to 'database_host', so it should match. However, this highlights the convention: always use lowercase keys in viper calls.",
    "assertions": [
      {
        "id": "7.1",
        "text": "Explains that viper normalizes all keys to lowercase internally"
      },
      {
        "id": "7.2",
        "text": "Recommends using lowercase keys consistently in viper.Get* calls"
      },
      {
        "id": "7.3",
        "text": "Identifies that viper.GetString(\"database_host\") is the correct form"
      },
      {
        "id": "7.4",
        "text": "Notes that while viper.GetString(\"DATABASE_HOST\") also works (due to lowercasing), using uppercase keys is a source of confusion"
      }
    ]
  },
  {
    "id": 8,
    "name": "duration-decode-hook",
    "description": "Tests that time.Duration fields in structs require a decode hook for Unmarshal to work",
    "prompt": "My Go viper config has 'timeout: 30s' in the YAML file. My struct has Timeout time.Duration and the mapstructure tag. After viper.Unmarshal, cfg.Timeout is 0. Why?",
    "trap": "Without the skill, the model may suggest using GetDuration or changing the config format to nanoseconds. The correct fix is registering StringToTimeDurationHookFunc via a decode hook option in Unmarshal.",
    "assertions": [
      {
        "id": "8.1",
        "text": "Identifies that mapstructure cannot decode a duration string into time.Duration without a hook"
      },
      {
        "id": "8.2",
        "text": "Shows viper.Unmarshal(&cfg, func(dc *mapstructure.DecoderConfig) { dc.DecodeHook = mapstructure.ComposeDecodeHookFunc(mapstructure.StringToTimeDurationHookFunc(), ...) })"
      },
      {
        "id": "8.3",
        "text": "Uses mapstructure.StringToTimeDurationHookFunc() as part of the hook"
      },
      {
        "id": "8.4",
        "text": "Does NOT suggest changing the config value to nanoseconds"
      }
    ]
  },
  {
    "id": 9,
    "name": "watch-config-atomic-rename-trap",
    "description": "Tests understanding of fsnotify behavior with editors that use atomic rename",
    "prompt": "I've set up viper.WatchConfig() in my Go service. When I test it by editing the config file in vim and saving, the OnConfigChange callback sometimes doesn't fire. What's happening?",
    "trap": "Without the skill, the model may suggest checking fsnotify version or debugging the callback. The root cause is that vim uses atomic rename (write-tmp, rename) which replaces the inode — fsnotify watches the inode and may miss or misfire for rename-based writes.",
    "assertions": [
      {
        "id": "9.1",
        "text": "Explains that vim and many editors write atomically via rename (write-to-temp, then rename over original)"
      },
      {
        "id": "9.2",
        "text": "Explains that fsnotify watches the inode, which is replaced by rename-based writes"
      },
      {
        "id": "9.3",
        "text": "Recommends testing hot-reload using direct writes (os.WriteFile) rather than editor saves"
      },
      {
        "id": "9.4",
        "text": "Does NOT suggest downgrading vim or switching to a different editor as the fix"
      }
    ]
  },
  {
    "id": 10,
    "name": "viper-alone-no-cobra",
    "description": "Tests that viper can be used without cobra for non-CLI services",
    "prompt": "I have a Go HTTP service (not a CLI) that should read config from a YAML file and environment variables. Should I use cobra alongside viper, or can I just use viper on its own?",
    "trap": "Without the skill, the model may suggest always pairing them or adding a minimal cobra setup. Viper is perfectly valid alone for services that have no CLI command tree.",
    "assertions": [
      {
        "id": "10.1",
        "text": "Clearly states that viper can be used without cobra"
      },
      {
        "id": "10.2",
        "text": "Explains cobra is for command trees/flags and is not needed for a simple HTTP service"
      },
      { "id": "10.3", "text": "Shows viper setup without any cobra imports" },
      {
        "id": "10.4",
        "text": "Does NOT recommend adding cobra just for configuration purposes"
      }
    ]
  },
  {
    "id": 11,
    "name": "unmarshal-key-vs-sub",
    "description": "Tests UnmarshalKey as a simpler alternative to Sub+Unmarshal",
    "prompt": "I want to unmarshal just the 'database' section of my viper config into a DatabaseConfig struct in Go. I've been using viper.Sub('database') then calling Unmarshal on the result. Is there a simpler way?",
    "trap": "Without the skill, the model may continue recommending Sub+Unmarshal without mentioning the nil risk or the cleaner alternative. UnmarshalKey avoids the Sub nil-check and is more direct.",
    "assertions": [
      {
        "id": "11.1",
        "text": "Recommends viper.UnmarshalKey(\"database\", &dbCfg) as the simpler alternative"
      },
      {
        "id": "11.2",
        "text": "Explains it avoids the nil check required with Sub()"
      },
      {
        "id": "11.3",
        "text": "Shows the correct usage: viper.UnmarshalKey(\"database\", &dbCfg)"
      },
      {
        "id": "11.4",
        "text": "If mentioning Sub+Unmarshal, notes the nil risk"
      }
    ]
  },
  {
    "id": 12,
    "name": "allow-empty-env",
    "description": "Tests AllowEmptyEnv behavior when an env var is set to empty string",
    "prompt": "In my Go service using viper, I set LOG_LEVEL='' (empty string) in my environment to override the config file value. But viper still returns the config file value 'info'. Why does the empty env var not take effect?",
    "trap": "Without the skill, the model may suggest checking AutomaticEnv or the prefix. By default, viper ignores empty-string env vars and continues down the precedence stack. AllowEmptyEnv(true) changes this behavior.",
    "assertions": [
      {
        "id": "12.1",
        "text": "Explains that viper treats empty string env vars as 'not set' by default"
      },
      {
        "id": "12.2",
        "text": "Introduces viper.AllowEmptyEnv(true) as the fix"
      },
      {
        "id": "12.3",
        "text": "Explains that with AllowEmptyEnv(true), an empty env var overrides the config file value"
      },
      {
        "id": "12.4",
        "text": "Does NOT suggest using viper.Set() as a workaround"
      }
    ]
  },
  {
    "id": 13,
    "name": "merge-in-config-layering",
    "description": "Tests MergeInConfig for base+override config file pattern",
    "prompt": "I want my Go service to ship with a built-in default config file, but let ops teams drop an override.yaml in /etc/myapp/ to customize specific values without copying the whole config. How do I implement this layered loading?",
    "trap": "Without the skill, the model may suggest reading only one file or writing custom merge logic. MergeInConfig is the viper primitive for this — base file first, then MergeInConfig for the override.",
    "assertions": [
      {
        "id": "13.1",
        "text": "Uses ReadInConfig for the base config and MergeInConfig for the override"
      },
      {
        "id": "13.2",
        "text": "Explains that keys from the override file win on collision"
      },
      {
        "id": "13.3",
        "text": "Does NOT suggest duplicating the entire config in the override file"
      },
      {
        "id": "13.4",
        "text": "Handles the case where the override file is missing (ConfigFileNotFoundError or os.Stat check)"
      }
    ]
  },
  {
    "id": 14,
    "name": "bind-env-non-prefixed-third-party",
    "description": "Tests BindEnv for env vars that don't follow the app prefix convention",
    "prompt": "My Go service uses viper with SetEnvPrefix('MYAPP') and AutomaticEnv(). I also need to read GOOGLE_APPLICATION_CREDENTIALS from the environment and expose it as viper key 'google.credentials'. The prefix makes AutomaticEnv look for MYAPP_GOOGLE_CREDENTIALS. How do I bind to the exact env var name?",
    "trap": "Without the skill, the model may suggest removing the prefix or reading via os.Getenv. BindEnv can bind a specific key to a specific env var name, bypassing the prefix.",
    "assertions": [
      {
        "id": "14.1",
        "text": "Uses viper.BindEnv(\"google.credentials\", \"GOOGLE_APPLICATION_CREDENTIALS\")"
      },
      {
        "id": "14.2",
        "text": "Explains BindEnv binds to the exact env var name, bypassing the prefix"
      },
      {
        "id": "14.3",
        "text": "Does NOT suggest removing SetEnvPrefix to fix this one case"
      },
      {
        "id": "14.4",
        "text": "Does NOT suggest using os.Getenv as the primary solution"
      }
    ]
  },
  {
    "id": 15,
    "name": "race-safe-on-config-change",
    "description": "Tests mutex protection for shared state updated in OnConfigChange callback",
    "prompt": "My Go service updates a global logLevel variable inside the viper.OnConfigChange callback. Under load I see data races flagged by the race detector. What's the correct pattern for updating shared state from a hot-reload callback?",
    "trap": "Without the skill, the model may only mention logging the error or ignoring the race. The correct pattern is protecting the shared state with sync.RWMutex — OnConfigChange runs in a background goroutine.",
    "assertions": [
      {
        "id": "15.1",
        "text": "Identifies that OnConfigChange runs in a background goroutine, causing the race"
      },
      {
        "id": "15.2",
        "text": "Uses sync.RWMutex (or sync.Mutex) to protect the shared state"
      },
      {
        "id": "15.3",
        "text": "Updates the shared state under Lock() inside OnConfigChange"
      },
      {
        "id": "15.4",
        "text": "Readers of the shared state use RLock()"
      }
    ]
  },
  {
    "id": 16,
    "name": "go-embed-default-config",
    "description": "Tests go:embed + viper.ReadConfig for shipping default config in the binary",
    "prompt": "I want my Go service binary to work out of the box without any config file on disk by including sensible defaults in a bundled YAML file. How do I ship a default config inside the binary and load it via viper?",
    "trap": "Without the skill, the model suggests calling viper.SetDefault for each key individually. The cleaner approach is //go:embed + viper.ReadConfig(bytes.NewReader(...)) which loads a full YAML file as defaults.",
    "assertions": [
      {
        "id": "16.1",
        "text": "Uses //go:embed to embed the YAML config file into the binary"
      },
      {
        "id": "16.2",
        "text": "Passes the embedded bytes to viper.ReadConfig(bytes.NewReader(...))"
      },
      {
        "id": "16.3",
        "text": "Calls viper.SetConfigType(\"yaml\") before ReadConfig"
      },
      {
        "id": "16.4",
        "text": "Does NOT suggest calling viper.SetDefault for each key as the primary approach"
      }
    ]
  },
  {
    "id": 17,
    "name": "validate-before-hot-reload-apply",
    "description": "Tests validate-then-swap pattern to protect against invalid hot-reloaded config",
    "prompt": "My Go service uses viper.WatchConfig(). A team member accidentally pushed a malformed config and my service began returning zero-values for all settings. How do I protect against invalid config being applied during a hot reload?",
    "trap": "Without the skill, the model may only suggest logging the error. The correct pattern is: unmarshal into a candidate struct, validate it, and only swap in the new config if validation passes — keep the previous config on failure.",
    "assertions": [
      {
        "id": "17.1",
        "text": "Unmarshals into a temporary candidate struct before applying"
      },
      {
        "id": "17.2",
        "text": "Validates the candidate config before overwriting the active config"
      },
      {
        "id": "17.3",
        "text": "Keeps the previous config unchanged when validation fails"
      },
      {
        "id": "17.4",
        "text": "Logs a clear error when the reload is rejected"
      }
    ]
  },
  {
    "id": 18,
    "name": "weakly-typed-input-env-bool",
    "description": "Tests WeaklyTypedInput for env vars that are always strings but map to bool struct fields",
    "prompt": "My Go service has a Config struct with an Enabled bool field and mapstructure tag. Setting MY_APP_ENABLED=true in the environment and calling viper.Unmarshal leaves cfg.Enabled as false, even though viper.GetBool works. Why?",
    "trap": "Without the skill, the model may suggest BindEnv or different env var naming. The issue is that env vars are always strings — mapstructure receives the string \"true\" and cannot decode it to bool without WeaklyTypedInput or a decode hook.",
    "assertions": [
      {
        "id": "18.1",
        "text": "Identifies that env vars are always strings and mapstructure cannot decode \"true\" to bool by default"
      },
      {
        "id": "18.2",
        "text": "Suggests enabling WeaklyTypedInput in the DecoderConfig or using a StringToBool decode hook"
      },
      {
        "id": "18.3",
        "text": "Shows viper.Unmarshal(&cfg, func(dc *mapstructure.DecoderConfig) { dc.WeaklyTypedInput = true }) or equivalent"
      },
      {
        "id": "18.4",
        "text": "Does NOT suggest changing the env var format or using only viper.GetBool as the fix"
      }
    ]
  }
]


path: references/binding-and-env.md
content: # Viper Env Binding and Flag Binding

## The binding interaction model

Three settings control how viper maps environment variables to keys. They must be set together:

```go
viper.SetEnvPrefix("MYAPP")                           // adds MYAPP_ prefix
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // database.host → MYAPP_DATABASE_HOST
viper.AutomaticEnv()                                   // activates auto-binding
```

Call these before any `ReadInConfig` or `viper.Get*` call — typically in a root command's `PersistentPreRunE` or in `init()`.

## AutomaticEnv vs BindEnv

| Method | Behavior |
| --- | --- |
| `AutomaticEnv()` | Every key is automatically mapped to its env equivalent (with prefix and replacer applied) |
| `BindEnv(key, envVars...)` | Only the specified key is bound, to the specified env var name(s) |

Use `AutomaticEnv` for the common case. Use `BindEnv` when you need to bind to an env var with a name that doesn't follow your prefix/replacer convention (e.g., third-party env vars like `GOOGLE_APPLICATION_CREDENTIALS`).

```go
// Bind a specific non-prefixed env var
viper.BindEnv("google.credentials", "GOOGLE_APPLICATION_CREDENTIALS")
```

## SetEnvKeyReplacer in depth

Viper keys use `.` as separator for nested values. Env vars cannot contain dots. The replacer maps between them.

```go
// Config file:
// database:
//   host: localhost
//   max_conn: 25

// Without replacer:
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
viper.GetString("database.host")  // looks for MYAPP_DATABASE.HOST — no match

// With replacer:
viper.SetEnvPrefix("MYAPP")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.GetString("database.host")  // looks for MYAPP_DATABASE_HOST — matches
```

The replacer operates on the viper key **before** prepending the prefix, so the lookup chain is: `database.host` → replace `.` with `_` → `database_host` → prepend prefix → `MYAPP_DATABASE_HOST`.

## AllowEmptyEnv

By default, viper ignores env vars set to the empty string — the empty string is treated as "not set" and viper continues down the precedence stack. Override this behavior:

```go
viper.AllowEmptyEnv(true)
// now MYAPP_PORT="" → viper.GetInt("port") == 0, not the default
```

## Flag binding

Bind a pflag after defining it:

```go
func init() {
    rootCmd.PersistentFlags().Int("port", 8080, "listen port")
    viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
}
```

Bind an entire flag set:

```go
viper.BindPFlags(rootCmd.PersistentFlags())
```

**Timing rule:** Bind flags in `init()` or in `PersistentPreRunE`. The binding call must happen before `Execute()` parses flags — specifically, before any `viper.Get*` call on a flag-backed key. Binding after `Execute()` causes the flag's `Changed` state to be unknown, so viper may not promote the flag value to the correct precedence layer.

## How pflag binding interacts with precedence

Viper checks `flag.Changed` (whether the user explicitly passed the flag). This is how it distinguishes between "flag default" (low priority) and "flag explicitly set" (high priority):

- `flag.Changed == false` (flag has its default): viper treats the flag as not present and falls through to env/file/default.
- `flag.Changed == true` (flag was provided on the command line): viper treats the flag value as the highest-priority source.

This means `viper.GetInt("port")` correctly returns the flag value when `--port 9090` is passed, and falls back to env `MYAPP_PORT` or config file `port: 8080` otherwise.

## Debugging binding

Print all resolved values to verify your binding is correct:

```go
fmt.Println(viper.AllSettings())
// map[database:map[host:localhost max_conn:25] port:8080]
```

Check env var resolution:

```go
os.Setenv("MYAPP_PORT", "9090")
viper.AutomaticEnv()
fmt.Println(viper.GetInt("port"))  // 9090
```


path: references/sources-and-formats.md
content: # Viper Config Sources and File Formats

## Supported file formats

Viper detects format from file extension. Supported extensions:

| Format     | Extensions      |
| ---------- | --------------- |
| YAML       | `.yaml`, `.yml` |
| TOML       | `.toml`         |
| JSON       | `.json`         |
| HCL        | `.hcl`          |
| INI        | `.ini`          |
| Properties | `.properties`   |
| dotenv     | `.env`          |

Force a format when there is no extension:

```go
viper.SetConfigType("yaml")
viper.SetConfigFile("/etc/myapp/config")  // no extension — type required
```

## Config file search

```go
viper.SetConfigName("config")            // file name without extension
viper.SetConfigType("yaml")              // required when no extension
viper.AddConfigPath("$HOME/.myapp")      // search path 1 (highest priority when multiple)
viper.AddConfigPath("/etc/myapp/")       // search path 2
viper.AddConfigPath(".")                 // search path 3 (lowest priority)

// viper searches paths in order, stops at the first match
if err := viper.ReadInConfig(); err != nil {
    var notFound *viper.ConfigFileNotFoundError
    if !errors.As(err, &notFound) {
        return err  // real error (permission denied, malformed YAML, etc.)
    }
    // not found — continue with flags/env/defaults
}

// After reading, this returns the resolved path:
fmt.Println("Using config:", viper.ConfigFileUsed())
```

## Merging multiple config files

`MergeInConfig` merges a second config file into the current state. Later values override earlier ones for the same key.

```go
viper.SetConfigFile("base.yaml")
viper.ReadInConfig()

viper.SetConfigFile("override.yaml")
viper.MergeInConfig()  // keys from override.yaml win on collision
```

Pattern: ship a base config with the binary, let users drop an override in `~/.myapp/override.yaml`.

## Multiple config files via SetConfigFile

For environment-based config loading:

```go
env := os.Getenv("APP_ENV")
if env == "" {
    env = "development"
}
viper.SetConfigFile(fmt.Sprintf("config.%s.yaml", env))
viper.ReadInConfig()
```

## Remote KV stores (etcd, Consul)

Viper supports remote KV stores via the `viper/remote` sub-package. This keeps remote config behind an opt-in import:

```go
import _ "github.com/spf13/viper/remote"

// etcd
viper.AddRemoteProvider("etcd3", "http://127.0.0.1:2379", "/config/myapp.yaml")
viper.SetConfigType("yaml")
viper.ReadRemoteConfig()

// Consul
viper.AddRemoteProvider("consul", "localhost:8500", "myapp/config")
viper.SetConfigType("json")
viper.ReadRemoteConfig()
```

**Caution:** Remote config adds network latency to startup and a runtime dependency. Use it only when you need centralized config across many service instances. For most applications, files + env vars are sufficient.

Watch for remote changes:

```go
go func() {
    for {
        time.Sleep(5 * time.Second)
        viper.WatchRemoteConfig()
        // re-read values after watching
    }
}()
```

## Embedding config with go:embed

Load config from embedded assets (for self-contained binaries):

```go
//go:embed config.yaml
var defaultConfig []byte

func init() {
    viper.SetConfigType("yaml")
    viper.ReadConfig(bytes.NewReader(defaultConfig))
}
```

`ReadConfig` accepts any `io.Reader`. This is useful for shipping default config inside the binary, then layering user overrides on top via `MergeInConfig`.


path: references/testing-and-isolation.md
content: # Viper Test Isolation

## The global state problem

The top-level `viper.*` functions operate on a global `*viper.Viper` instance shared across all tests in the same process. Tests that call `viper.SetConfigFile`, `viper.Set`, or `viper.ReadInConfig` pollute this global state, causing flaky test ordering.

```go
// ✗ Bad — sets global state that affects later tests
func TestPortConfig(t *testing.T) {
    viper.SetDefault("port", 8080)
    viper.Set("port", 9090)
    assert.Equal(t, 9090, viper.GetInt("port"))
    // global viper now has port=9090 for all subsequent tests
}
```

## viper.New() per test (correct approach)

```go
func TestPortConfig(t *testing.T) {
    v := viper.New()
    v.SetDefault("port", 8080)
    v.Set("port", 9090)
    assert.Equal(t, 9090, v.GetInt("port"))
}

func TestDefaultPort(t *testing.T) {
    v := viper.New()
    v.SetDefault("port", 8080)
    assert.Equal(t, 8080, v.GetInt("port"))  // clean — not affected by TestPortConfig
}
```

## Injecting viper into your app

For test isolation to work, your application code must accept a `*viper.Viper` instead of calling the global functions directly:

```go
// ✓ Good — accepts a viper instance
type Server struct {
    cfg *viper.Viper
}

func NewServer(v *viper.Viper) *Server {
    return &Server{cfg: v}
}

func (s *Server) Port() int {
    return s.cfg.GetInt("port")
}

// In tests:
func TestServer(t *testing.T) {
    v := viper.New()
    v.Set("port", 9090)
    s := NewServer(v)
    assert.Equal(t, 9090, s.Port())
}

// In main:
func main() {
    // viper setup...
    s := NewServer(viper.GetViper())  // pass the global instance in production
}
```

## Reading config files in tests

```go
func TestReadConfig(t *testing.T) {
    v := viper.New()
    v.SetConfigFile("testdata/config.yaml")
    require.NoError(t, v.ReadInConfig())
    assert.Equal(t, "localhost", v.GetString("host"))
}
```

Use `testdata/` for config files. Go test tooling sets the working directory to the package directory, so relative paths work reliably.

## t.Setenv interactions

`t.Setenv` sets an env var for the duration of a test and restores it on cleanup. Combined with `viper.New()` + `AutomaticEnv`, this lets you test env var binding without global pollution:

```go
func TestEnvBinding(t *testing.T) {
    t.Setenv("MYAPP_PORT", "9090")

    v := viper.New()
    v.SetEnvPrefix("MYAPP")
    v.AutomaticEnv()

    assert.Equal(t, 9090, v.GetInt("port"))
    // t.Setenv restores original MYAPP_PORT (or unsets it) after this test
}
```

## viper.Reset() — use with caution

`viper.Reset()` resets the global viper instance to its zero state. It is rarely the right solution:

- It affects all code running concurrently that also uses the global viper.
- It does not stop any active `WatchConfig` goroutines.
- Using it in `TestMain` or `t.Cleanup` makes tests order-dependent.

Prefer `viper.New()` per test. Reserve `Reset()` for tools that call into viper-based libraries and must restore state between runs.

## Snapshot and restore pattern

When you cannot refactor to inject `*viper.Viper` and must use the global:

```go
func snapshotViper() map[string]interface{} {
    return viper.AllSettings()
}

func restoreViper(snapshot map[string]interface{}) {
    viper.Reset()
    for k, v := range snapshot {
        viper.Set(k, v)
    }
}

func TestWithGlobalViper(t *testing.T) {
    snapshot := snapshotViper()
    t.Cleanup(func() { restoreViper(snapshot) })

    viper.Set("port", 9090)
    // test code...
}
```

This approach is fragile — `AllSettings()` only captures the resolved values, not the binding state (defaults, env bindings, etc.). Prefer injection.


path: references/unmarshal.md
content: # Viper Unmarshal and Struct Mapping

## Basic Unmarshal

```go
type Config struct {
    Port     int    `mapstructure:"port"`
    Host     string `mapstructure:"host"`
    LogLevel string `mapstructure:"log_level"`
    Database struct {
        DSN     string `mapstructure:"dsn"`
        MaxConn int    `mapstructure:"max_conn"`
    } `mapstructure:"database"`
}

var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
    return fmt.Errorf("decoding config: %w", err)
}
```

## mapstructure tags

Always use `mapstructure` tags. Without them, mapstructure falls back to case-insensitive field name matching, which works for simple cases but silently fails for:

- Nested structs where the outer key uses an underscore (`max_conn` → `MaxConn`)
- Unexported fields
- Fields where the Go name does not match the config key

```go
// ✓ Good — explicit and immune to rename surprises
type TLSConfig struct {
    CertFile string `mapstructure:"cert_file"`
    KeyFile  string `mapstructure:"key_file"`
    Enabled  bool   `mapstructure:"enabled"`
}

// ✗ Fragile — relies on case-folding; breaks when config key uses underscores
type TLSConfig struct {
    CertFile string  // viper key "certfile" or "CertFile", not "cert_file"
    KeyFile  string
    Enabled  bool
}
```

## UnmarshalKey — extracting a sub-tree

```go
type DatabaseConfig struct {
    DSN     string `mapstructure:"dsn"`
    MaxConn int    `mapstructure:"max_conn"`
}

var dbCfg DatabaseConfig
if err := viper.UnmarshalKey("database", &dbCfg); err != nil {
    return fmt.Errorf("decoding database config: %w", err)
}
```

Prefer `UnmarshalKey` over `viper.Sub` + `Unmarshal` — fewer nil checks and less boilerplate.

## time.Duration

Viper's `GetDuration` parses duration strings (`"1h30m"`, `"500ms"`) from config files and env vars. When using `Unmarshal`, mapstructure does not know how to decode a duration string into `time.Duration` by default.

Register a decode hook:

```go
import "github.com/mitchellh/mapstructure"

var cfg Config
err := viper.Unmarshal(&cfg, func(dc *mapstructure.DecoderConfig) {
    dc.DecodeHook = mapstructure.ComposeDecodeHookFunc(
        mapstructure.StringToTimeDurationHookFunc(),
        mapstructure.StringToSliceHookFunc(","),
        dc.DecodeHook,
    )
})
```

`StringToTimeDurationHookFunc` handles `"1h30m"` → `time.Duration`. `StringToSliceHookFunc(",")` handles `"a,b,c"` → `[]string{"a", "b", "c"}`.

## net.IP and custom types

```go
import "github.com/mitchellh/mapstructure"

func stringToIPHookFunc() mapstructure.DecodeHookFunc {
    return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
        if f.Kind() != reflect.String || t != reflect.TypeOf(net.IP{}) {
            return data, nil
        }
        ip := net.ParseIP(data.(string))
        if ip == nil {
            return nil, fmt.Errorf("invalid IP address: %s", data)
        }
        return ip, nil
    }
}
```

## Squash for embedded structs

```go
type BaseConfig struct {
    LogLevel string `mapstructure:"log_level"`
    Debug    bool   `mapstructure:"debug"`
}

type ServerConfig struct {
    BaseConfig `mapstructure:",squash"`  // merge BaseConfig fields at this level
    Port       int    `mapstructure:"port"`
}
```

Without `,squash`, the base config must be nested under a `baseconfig` key in the config file.

## Remain for unknown keys

```go
type Config struct {
    Port     int                    `mapstructure:"port"`
    Remain   map[string]interface{} `mapstructure:",remain"`
}
```

Extra keys from the config file are collected in `Remain` instead of being silently dropped. Useful for forward compatibility.

## Weak decoding

Enable weak type decoding (e.g., string `"true"` → bool `true`) when working with env vars that are always strings:

```go
var cfg Config
err := viper.Unmarshal(&cfg, func(dc *mapstructure.DecoderConfig) {
    dc.WeaklyTypedInput = true
})
```

Use with caution — weak decoding can hide bugs where a wrong value type is silently converted.


path: references/watch-and-reload.md
content: # Viper WatchConfig and Hot Reload

## Basic setup

```go
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
    log.Printf("config changed: %s (op: %s)", e.Name, e.Op)
    // re-read affected values and apply them
})
```

`WatchConfig` starts a background goroutine that watches the config file using fsnotify. Call it after `ReadInConfig`.

## The atomic-rename trap

Most editors (vim, neovim, many CI tools) write config files by creating a new file and then renaming it over the old one. This replaces the inode that fsnotify is watching — the watch may not fire, or may fire with `Op: RENAME` instead of `Op: WRITE`, or may fire twice.

**Test hot reload with direct file writes, not editor saves:**

```go
// reliable in tests:
os.WriteFile("config.yaml", newContent, 0644)

// unreliable for testing:
// opening vim and :w — may trigger RENAME instead of WRITE
```

In production, this is less of an issue if your config management tool (Kubernetes ConfigMap volume mount, Consul Template, etc.) is aware of inode behavior.

## Race-safe reload pattern

Config reload happens in a background goroutine. Any shared state updated in `OnConfigChange` must be synchronized:

```go
type Config struct {
    mu       sync.RWMutex
    LogLevel string `mapstructure:"log_level"`
    MaxConn  int    `mapstructure:"max_conn"`
}

var cfg Config

viper.OnConfigChange(func(e fsnotify.Event) {
    var newCfg Config
    if err := viper.Unmarshal(&newCfg); err != nil {
        log.Printf("error reloading config: %v", err)
        return  // keep old config on error
    }

    cfg.mu.Lock()
    cfg.LogLevel = newCfg.LogLevel
    cfg.MaxConn = newCfg.MaxConn
    cfg.mu.Unlock()

    log.Printf("config reloaded: log_level=%s", newCfg.LogLevel)
})
```

Reads use `appCfg.mu.RLock()`. Never read directly from viper in hot paths during reload — the window between `OnConfigChange` firing and viper updating its internal state is non-deterministic.

## Debouncing rapid changes

Some filesystems fire multiple events per save. Debounce to avoid reloading multiple times:

```go
var reloadTimer *time.Timer
var reloadMu sync.Mutex

viper.OnConfigChange(func(e fsnotify.Event) {
    reloadMu.Lock()
    defer reloadMu.Unlock()
    if reloadTimer != nil {
        reloadTimer.Stop()
    }
    reloadTimer = time.AfterFunc(100*time.Millisecond, func() {
        applyNewConfig()
    })
})
```

## Validating config before applying

Always validate reloaded config before applying it — an invalid config mid-reload should keep the previous working config:

```go
viper.OnConfigChange(func(e fsnotify.Event) {
    var candidate Config
    if err := viper.Unmarshal(&candidate); err != nil {
        log.Printf("reload: invalid config, keeping previous: %v", err)
        return
    }
    if err := validate(candidate); err != nil {
        log.Printf("reload: validation failed, keeping previous: %v", err)
        return
    }
    applyConfig(candidate)
})
```

## Stopping the watcher

There is no documented way to stop `WatchConfig` once started. Design your application so that the watcher's lifetime matches the process lifetime. For testing, create a new `viper.New()` instance per test — the watcher is per-instance and is garbage-collected with the instance.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)