{"id":"a1e65153-1faa-495a-963b-61885b4f1830","shortId":"qMWULS","kind":"skill","title":"golang-spf13-viper","tagline":"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, WatchCon","description":"**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.\n\n# Using spf13/viper for layered configuration in Go\n\nViper 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.\n\n**Official Resources:**\n\n- [pkg.go.dev/github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper)\n- [github.com/spf13/viper](https://github.com/spf13/viper)\n\nThis skill is not exhaustive. Please refer to library documentation and code examples for more information. Context7 can help as a discoverability platform.\n\n```bash\ngo get github.com/spf13/viper@latest\n```\n\n## Viper vs. cobra\n\nCobra 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`.\n\n→ See `samber/cc-skills-golang@golang-spf13-cobra` for the cobra side of this integration.\n\n## The precedence pipeline\n\nViper resolves a key by walking sources in this order (first set value wins):\n\n```\n1. explicit Set()      — viper.Set(\"key\", val)    highest priority\n2. flag                — bound pflag.Flag\n3. env var             — BindEnv / AutomaticEnv\n4. config file         — ReadInConfig / MergeInConfig\n5. KV remote           — etcd / Consul\n6. default             — viper.SetDefault(\"key\", val)   lowest priority\n```\n\nThis 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.\n\n## Sources and config files\n\n```go\nviper.SetConfigName(\"config\")\nviper.AddConfigPath(\"$HOME/.myapp\")\nif err := viper.ReadInConfig(); err != nil {\n    var notFound *viper.ConfigFileNotFoundError\n    if !errors.As(err, &notFound) {\n        return fmt.Errorf(\"reading config: %w\", err) // propagate real errors only\n    }\n}\n```\n\n`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.\n\nFor supported formats (JSON, TOML, YAML, HCL, INI, properties), `MergeInConfig`, and remote KV, see [sources-and-formats.md](references/sources-and-formats.md).\n\n## Env binding and key replacers\n\nThis is the highest-bug-density area in viper. All three settings must be wired together — missing any one breaks nested key resolution:\n\n```go\n// ✓ Good — all three wired together at startup\nviper.SetEnvPrefix(\"MYAPP\")                             // prevent collisions: PORT → MYAPP_PORT\nviper.SetEnvKeyReplacer(strings.NewReplacer(\".\", \"_\"))  // database.host → MYAPP_DATABASE_HOST\nviper.AutomaticEnv()\n\n// ✗ Bad — without SetEnvKeyReplacer, viper looks for MYAPP_DATABASE.HOST (dot preserved)\n```\n\nFor `BindEnv`, `AllowEmptyEnv`, and env-vs-default interaction, see [binding-and-env.md](references/binding-and-env.md).\n\n## Flag binding (the cobra seam)\n\nBind cobra flags to viper in `init()` or `PersistentPreRunE` — never in `RunE` (too late; cobra parses flags before `RunE` runs):\n\n```go\nfunc init() {\n    rootCmd.PersistentFlags().Int(\"port\", 8080, \"listen port\")\n    viper.BindPFlag(\"port\", rootCmd.PersistentFlags().Lookup(\"port\"))\n    // viper.BindPFlags(cmd.Flags()) — bind an entire FlagSet at once\n}\n```\n\nFor `AllowEmptyEnv` and flag/env interaction details, see [binding-and-env.md](references/binding-and-env.md).\n\n## Unmarshaling into structs\n\n`viper.Unmarshal` maps the resolved configuration into a struct using `mapstructure`:\n\n```go\ntype Config struct {\n    Port     int `mapstructure:\"port\"`\n    Database struct {\n        MaxConn int `mapstructure:\"max_conn\"` // explicit tag: mapstructure won't convert underscore→camelCase\n    } `mapstructure:\"database\"`\n}\nvar cfg Config\nviper.Unmarshal(&cfg)\n```\n\n**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.\n\nFor `time.Duration` / `net.IP` / slice decoders and custom `DecodeHook` registration, see [unmarshal.md](references/unmarshal.md).\n\n## Sub-trees\n\n`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.\n\n## Hot reload\n\n```go\nviper.WatchConfig()\nviper.OnConfigChange(func(e fsnotify.Event) { /* re-apply changed values */ })\n```\n\n`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).\n\n## Test isolation\n\n**Never use the global viper in tests** — state leaks across test cases. Use `viper.New()` per test so each instance is isolated:\n\n```go\nv := viper.New()\nv.SetConfigFile(\"testdata/config.yaml\")\nrequire.NoError(t, v.ReadInConfig())\n```\n\nFor `t.Setenv` interactions and `Reset()` limitations, see [testing-and-isolation.md](references/testing-and-isolation.md).\n\n## Best Practices\n\n1. **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`).\n2. **Handle `ConfigFileNotFoundError` gracefully** — a missing config file should not crash a service that runs with only flags and env vars.\n3. **Always use `mapstructure` tags on config structs** — implicit mapping silently misses nested and underscore-named fields.\n4. **Use `viper.New()` in tests, never the global** — the global accumulates state across test runs; per-test instances are isolated.\n5. **Bind flags before `Execute()`** — binding in `RunE` is too late; cobra parses flags before `RunE` runs.\n\n## Common Mistakes\n\n| Mistake | Why it fails | Fix |\n| --- | --- | --- |\n| `AutomaticEnv` without `SetEnvKeyReplacer` | `database.host` looks for `MYAPP_DATABASE.HOST` (dot preserved) — never matches | Add `SetEnvKeyReplacer(strings.NewReplacer(\".\", \"_\"))` before `AutomaticEnv` |\n| No `mapstructure` tags on struct fields | Silently misses nested and underscore-named fields | Add `mapstructure:\"key_name\"` to every field |\n| Using global viper in tests | State from one test contaminates the next, causing flaky ordering | Create `viper.New()` per test |\n| 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 |\n\n## Further Reading\n\n- [sources-and-formats.md](references/sources-and-formats.md) — supported file formats, multi-path search, MergeInConfig, remote KV (etcd/Consul)\n- [binding-and-env.md](references/binding-and-env.md) — BindEnv, AutomaticEnv, SetEnvPrefix, SetEnvKeyReplacer, AllowEmptyEnv, timing rules\n- [unmarshal.md](references/unmarshal.md) — Unmarshal, UnmarshalKey, mapstructure tags, custom DecodeHooks (Duration, IP, slice)\n- [watch-and-reload.md](references/watch-and-reload.md) — WatchConfig, OnConfigChange, fsnotify caveats, atomic-rename trap, race-safe patterns\n- [testing-and-isolation.md](references/testing-and-isolation.md) — viper.New() per test, t.Setenv interactions, Reset() limitations, snapshot/restore\n\n## Cross-References\n\n- → See `samber/cc-skills-golang@golang-cli` skill for general CLI architecture — project layout, exit codes, signal handling, cobra+viper integration\n- → See `samber/cc-skills-golang@golang-spf13-cobra` skill for the cobra side of this integration (flag definition and binding)\n- → See `samber/cc-skills-golang@golang-testing` skill for general Go testing patterns\n\nIf you encounter a bug or unexpected behavior in spf13/viper, open an issue at <https://github.com/spf13/viper/issues>.","tags":["golang","spf13","viper","skills","samber","agent","agent-skills","antigravity","claude","claude-code","code","codex"],"capabilities":["skill","source-samber","skill-golang-spf13-viper","topic-agent","topic-agent-skills","topic-antigravity","topic-claude","topic-claude-code","topic-code","topic-codex","topic-coding","topic-copilot","topic-cursor","topic-gemini","topic-gemini-cli-extension"],"categories":["cc-skills-golang"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/samber/cc-skills-golang/golang-spf13-viper","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add samber/cc-skills-golang","source_repo":"https://github.com/samber/cc-skills-golang","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 1725 github stars · SKILL.md body (8,475 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T18:53:02.962Z","embedding":null,"createdAt":"2026-05-01T12:52:37.154Z","updatedAt":"2026-05-18T18:53:02.962Z","lastSeenAt":"2026-05-18T18:53:02.962Z","tsv":"'/github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper)':128 '/spf13/viper/issues':1084 '/spf13/viper@latest':160 '/spf13/viper](https://github.com/spf13/viper)':131 '1':263,769 '2':271,793 '3':275,814 '4':280,832 '5':285,853 '6':290 '8080':515 'accumul':842 'across':738,844 'add':888,907 'allowemptyenv':474,532,979 'alon':209,216,947 'alway':583,653,815 'answer':104,179 'api':66 'appli':683 'architectur':1029 'area':424 'arg':171 'atom':695,1000 'atomic-renam':999 'automaticenv':20,279,774,877,892,976 'avoid':608,668 'bad':463 'bash':155 'beat':47,49,51 'behavior':1075 'best':767 'bind':55,227,413,485,489,525,854,858,1056 'bindenv':278,473,975 'binding-and-env.md':482,538,973 'bindpflag':232 'bindpflag/bindpflags':17 'bound':273 'break':437 'bug':310,422,1072 'call':658 'callback':704 'camelcas':575 'cannot':302 'case':740 'caus':779,926 'caveat':998 'cfg':579,582 'chang':684 'check':612,656,935 'cli':1024,1028 'clis':214 'cmd.flags':524 'cobra':163,164,208,238,241,487,490,503,864,1036,1044,1048 'code':143,1033 'collis':452 'come':315 'command':97,167 'common':870 'complet':173 'config':219,281,318,336,340,358,370,555,580,799,820,937 'config-fil':218 'config.yaml':714 'configfilenotfounderror':22,365,795,934 'configur':6,41,71,76,176,547 'conn':567 'consul':289 'contamin':923 'context7':148 'convert':573 'crash':382,803,939 'creat':929 'cross':1018 'cross-refer':1017 'custom':626,988 'daemon':221 'databas':460,561,577,601,605,636,665,791 'database.host':458,787,788,880 'dbcfg':602,666 'decod':624 'decodehook':627,989 'default':16,52,291,332,479 'defin':96 'definit':1054 'densiti':423 'detail':536 'discover':153 'document':141 'doesn':94 'dot':470,884 'durat':990 'e':679 'echo':713 'editor':692,716 'encount':1070 'engin':38 'entir':527,672 'env':13,48,276,325,394,412,477,781,812 'env-vs-default':476 'err':344,346,353,360,949 'error':363,377,957 'errors.as':352,948 'etcd':288 'etcd/consul':972 'everi':56,912 'exampl':144 'execut':857 'exhaust':136 'exist':652 'exit':1032 'explicit':264,568 'face':91,197 'fail':875 'field':598,831,898,906,913 'file':14,50,220,282,319,337,371,381,800,938,963 'fire':707 'first':259 'fix':83,300,876 'flag':12,46,99,170,212,228,272,329,392,484,491,505,810,855,866,1053 'flag-on':211 'flag/env':534 'flags/env':946 'flagset':528 'flaki':927 'fmt.errorf':356 'format':398,964 'found':956 'four':60 'fragil':590 'fsnotifi':688,997 'fsnotify.event':680 'func':510,678 'general':1027,1064 'get':157 'github.com':130,159,1083 'github.com/spf13/viper/issues':1082 'github.com/spf13/viper@latest':158 'github.com/spf13/viper](https://github.com/spf13/viper)':129 'global':732,839,841,915 'go':37,73,156,338,441,509,553,675,750,1065 'golang':2,5,236,1023,1042,1060 'golang-c':1022 'golang-spf13-cobra':235,1041 'golang-spf13-viper':1 'golang-test':1059 'good':442 'grace':369,796 'handl':368,794,1035 'hcl':402 'help':150 'highest':120,269,421 'highest-bug-dens':420 'home/.myapp':342 'host':461,792 'hot':673,710 'hot-reload':709 'implicit':587,822 'inform':147 'ini':403 'init':495,511 'inod':691,702 'instanc':747,850 'instead':789 'int':513,558,564 'integr':245,1038,1052 'interact':480,535,760,1013 'ip':991 'isol':728,749,852 'issu':1080 'job':101 'json':399 'key':57,110,185,204,252,267,293,312,415,439,617,649,772,782,909 'key-valu':203 'kv':15,286,408,971 'late':502,863 'layer':10,44,61,70,118,191 'layout':1031 'leak':737 'librari':7,140 'limit':763,1015 'listen':516 'look':467,881 'lookup':521 'lowest':122,295 'map':544,588,823 'mapstructur':24,552,559,565,570,576,585,817,894,908,986 'match':887 'max':566 'maxconn':563 'may':320,705 'mergeinconfig':284,405,969 'method':659 'miss':380,434,619,776,798,825,900,933,936 'mistak':871,872 'multi':966 'multi-path':965 'multipl':79 'must':366,430 'myapp':450,454,459 'myapp_database.host':469,883 'name':597,830,905,910 'need':225 'neovim':699 'nest':438,592,780,826,901 'net.ip':622 'never':498,729,837,886 'new':639 'next':925 'nil':347,611,646,655,670 'nil-check':610,654 'non':954 'non-not-found':953 'notfound':349,354,950 'offici':124 'onconfigchang':996 'one':65,436,778,921 'open':1078 'option':374 'order':85,258,928 'own':165,175 'pars':504,865 'path':967 'pattern':723,1006,1067 'per':743,848,931,1010 'per-test':847 'perfect':386 'persistentprerun':230,497 'persona':33 'pflag.flag':274 'pipelin':248,298 'pkg.go.dev':127 'pkg.go.dev/github.com/spf13/viper](https://pkg.go.dev/github.com/spf13/viper)':126 'platform':154 'pleas':137 'port':453,455,514,517,519,522,557,560 'practic':768 'preced':11,84,247 'prefer':599,663 'prefix':644,771 'preserv':471,885 'prevent':307,451 'prioriti':123,270,296 'program':383 'project':1030 'propag':361,952 'properti':404 'pure':201 'race':720,1004 'race-saf':719,1003 're':682 're-appli':681 'reachabl':63 'read':357,959 'readinconfig':21,283 'real':362 'refer':138,1019 'references/binding-and-env.md':483,539,974 'references/sources-and-formats.md':411,961 'references/testing-and-isolation.md':766,1008 'references/unmarshal.md':631,983 'references/watch-and-reload.md':726,994 'registr':628 'reload':674,711,722 'remot':287,407,970 'renam':697,1001 'reorder':304 'replac':416,700,773 'requir':614 'require.noerror':755 'reset':762,1014 'resolut':177,440 'resolv':75,206,250,546,786 'resourc':125 'result':662 'return':355,637 'right':112 'risk':671 'rootcmd.persistentflags':512,520 'rule':981 'run':389,508,807,846,869,944 'rune':500,507,860,868 'safe':721,1005 'samber/cc-skills-golang':234,1021,1040,1058 'save':717 'scope':641 'seam':488 'search':968 'see':233,409,481,537,629,724,764,1020,1039,1057 'servic':805,941 'set':260,265,429,770 'setenvkeyreplac':19,465,879,889,978 'setenvprefix':18,977 'shadow':322 'side':242,1049 'signal':1034 'silent':784,824,899 'skill':133,1025,1045,1062 'skill-golang-spf13-viper' 'slice':623,992 'snapshot/restore':1016 'sourc':80,117,190,255,334 'source-samber' 'sources-and-formats.md':410,960 'spf13':3,237,1043 'spf13/viper':9,68,1077 'startup':448 'state':736,843,919 'stay':62 'strings.newreplacer':457,890 'struct':25,542,550,556,562,593,821,897 'sub':27,30,604,613,633 'sub-tre':29,632 'subcommand':169 'support':397,962 'surfac':92,198 'system':45 't.setenv':759,1012 'tag':26,569,586,818,895,987 'test':708,727,735,739,744,836,845,849,918,922,932,1011,1061,1066 'testdata/config.yaml':754 'testing-and-isolation.md':765,1007 'three':428,444 'time':980 'time.duration':621 'togeth':433,446,775 'toml':400 'topic-agent' 'topic-agent-skills' 'topic-antigravity' 'topic-claude' 'topic-claude-code' 'topic-code' 'topic-codex' 'topic-coding' 'topic-copilot' 'topic-cursor' 'topic-gemini' 'topic-gemini-cli-extension' 'trap':1002 'treat':40 'tree':31,168,634 'type':554 'underscor':574,596,829,904 'underscore-nam':595,828,903 'understand':305 'unexpect':1074 'unhandl':376 'unmarsh':23,540,606,984 'unmarshal.md':630,982 'unmarshalkey':600,664,985 'use':8,67,207,551,584,687,730,741,816,833,914 'user':90,196 'user-fac':89,195 'usual':373 'v':751 'v.readinconfig':757 'v.setconfigfile':753 'val':268,294 'valid':172,387 'valu':77,108,183,205,261,333,685 'var':277,326,348,395,578,813 'via':231,696 'vim':698 'viper':4,74,161,174,192,215,249,309,426,466,493,733,916,1037 'viper.addconfigpath':341 'viper.automaticenv':462 'viper.bindpflag':518 'viper.bindpflags':523 'viper.configfilenotfounderror':350 'viper.new':742,752,834,930,1009 'viper.onconfigchange':677 'viper.readinconfig':345 'viper.set':266 'viper.setconfigname':339 'viper.setdefault':292 'viper.setenvkeyreplacer':456 'viper.setenvprefix':449 'viper.sub':635 'viper.unmarshal':543,581 'viper.viper':640 'viper.watchconfig':676 'vs':162,478 'w':359 'walk':115,188,254 'watch':690 'watch-and-reload.md':725,993 'watchcon':32 'watchconfig':686,995 'win':262 'wire':432,445 'without':464,878 'won':571 'write':694 'x':111,186 'yaml':401","prices":[{"id":"2c930070-addc-4a7b-a92f-185fd92a145a","listingId":"a1e65153-1faa-495a-963b-61885b4f1830","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"samber","category":"cc-skills-golang","install_from":"skills.sh"},"createdAt":"2026-05-01T12:52:37.154Z"}],"sources":[{"listingId":"a1e65153-1faa-495a-963b-61885b4f1830","source":"github","sourceId":"samber/cc-skills-golang/golang-spf13-viper","sourceUrl":"https://github.com/samber/cc-skills-golang/tree/main/skills/golang-spf13-viper","isPrimary":false,"firstSeenAt":"2026-05-01T12:52:37.154Z","lastSeenAt":"2026-05-18T18:53:02.962Z"},{"listingId":"a1e65153-1faa-495a-963b-61885b4f1830","source":"skills_sh","sourceId":"samber/cc-skills-golang/golang-spf13-viper","sourceUrl":"https://skills.sh/samber/cc-skills-golang/golang-spf13-viper","isPrimary":true,"firstSeenAt":"2026-05-07T20:42:05.463Z","lastSeenAt":"2026-05-07T22:41:23.642Z"}],"details":{"listingId":"a1e65153-1faa-495a-963b-61885b4f1830","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"samber","slug":"golang-spf13-viper","github":{"repo":"samber/cc-skills-golang","stars":1725,"topics":["agent","agent-skills","ai","antigravity","claude","claude-code","code","codex","coding","copilot","cursor","gemini","gemini-cli-extension","openclaw","opencode","plugin","skills","skillsmp","vibe-coding"],"license":"mit","html_url":"https://github.com/samber/cc-skills-golang","pushed_at":"2026-05-18T17:36:00Z","description":"🧑‍🎨 A collection of Golang agentic skills that works","skill_md_sha":"591508c871dfaad2bcbc17614842059ef7ae15ae","skill_md_path":"skills/golang-spf13-viper/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/samber/cc-skills-golang/tree/main/skills/golang-spf13-viper"},"layout":"multi","source":"github","category":"cc-skills-golang","frontmatter":{"name":"golang-spf13-viper","license":"MIT","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`.","compatibility":"Designed for Claude Code or similar AI coding agents, and for projects using Golang."},"skills_sh_url":"https://skills.sh/samber/cc-skills-golang/golang-spf13-viper"},"updatedAt":"2026-05-18T18:53:02.962Z"}}