Figtree: a Go config manager with validators, callbacks and mutation tracking
A stdlib-flag configuration library for Go that reads YAML/JSON/INI and env vars, built as a Viper alternative.
Figtree is a configuration library that wants to be your application's single source of runtime settings. You register typed properties — strings, ints, bools, durations, lists, maps — and figtree wires them up as command-line flags (via the standard flag package), reads environment variables, and loads YAML, JSON or INI config files. It is written in Go 1.23, released under MIT, and its author positions it explicitly as a replacement for Viper.
The naming leans into a botany metaphor that runs through the whole API: you *grow* a tree, properties are *mutageneses*, their values are *fruit* (Fig{}), and tracking can be paused with Curse() and resumed with Recall(). The README is candid about where it stands versus Viper — file watching, struct unmarshaling and remote sources are still planned; per-property validators, callbacks and mutation channels are present today.
Getting started
Installation follows the usual module path:
go get -u github.com/andreimerlescu/figtree/v2
You construct a tree one of three ways. New() does no mutation tracking; Grow() enables it; With(figtree.Options{...}) lets you set options explicitly, including Tracking, Pollinate (read env vars on getters), Harvest, IgnoreEnvironment, Germinate and a ConfigFile path.
figs := figtree.Grow()
figs.NewUnitDuration("minutes", 1, time.Minute, "minutes for timer")
figs.WithValidator("minutes", figtree.AssureDurationLessThan(time.Hour))
err := figs.Parse()
if err != nil {
log.Fatal(err)
}
log.Println(*figs.UnitDuration("minutes"))
Properties are registered before parsing with New<Type>() calls such as NewString, NewInt, NewFloat64, NewBool, NewList and NewMap. Each has a getter (*figs.String(key)), a setter (figs.Store(tString, key, value)) and a fruit accessor (figs.Fig(key)). The README notes that Duration and UnitDuration are interchangeable since both rely on *time.Duration.
Validators and callbacks
The headline feature is per-property validation. The README claims 36 built-in validator functions, though its own available-validators table lists more; use Assure... names, often in an Assure<Type><Rule> pattern — for example AssureStringNotEmpty, AssureIntInRange, AssureFloat64NotNaN, AssureDurationMax and list/map checks like AssureListContains and AssureMapHasKey. A custom validator takes a func (interface{}) error signature.
The README's main text says only one validator is permitted per property, and that a duplicate WithValidator() call records an error on the property's Fig.Error — though its version history notes v2.0.0 enabled multiple validators per key.
Callbacks attach to lifecycle events through WithCallback(key, figtree.CallbackAfterX, func(value interface{}) error). The README documents three trigger points:
| Option | When it runs | |---|---| | CallbackAfterVerify | On .Parse(), .ParseFile(), Load() or LoadFile() | | CallbackAfterRead | On getters like figs.String(key) | | CallbackAfterChange | On .Store(...) and .Resurrect(key) |
The README warns that callbacks can slow figtree down noticeably when used heavily.
Rules
Beyond validators, a rule system changes how properties behave. Global rules are set with WithTreeRule, property-level ones with WithRule. Available kinds include RulePreventChange (blocks store methods), RulePanicOnChange, RuleNoValidations, RuleNoCallbacks, RuleCondemnedFromResurrection, plus switches to disable maps, lists, flags or environment handling entirely.
figs := figtree.Grow()
figs.WithTreeRule(figtree.RuleNoFlags)
figs.NewString("name", "", "your name")
figs.WithValidator("name", figtree.AssureStringNotEmpty)
figs.WithRule("name", figtree.RuleNoValidations) // turn off validations
err := figs.Parse()
Mutation tracking
With Tracking enabled, every change to a property value is sent as a Mutation on the channel returned by Mutations(). A typical pattern runs a goroutine ranging over that channel and logging changes. The README's complex example shows how this catches an environment variable changing under you: after export workers=20, it logs workers changed from 15 to 20.
Tracking can be paused with Curse() (which closes the mutations channel) and resumed with Recall(). Undefined keys are handled by Resurrect(), which creates a new entry, checking environment and files first.
Loading order
Figtree resolves values through several sources. The README's worked example shows precedence: env vars can override both file values and CLI arguments — the README's worked example carries the note "ENV overrides yaml and args." Load() tries ./config.yaml, ./config.json or ./config.ini automatically unless CONFIG_FILE is set, while ParseFile("file") reads a specific file by extension. Passing an empty string to Parse() parses only command-line arguments.
The package also exposes Usage(), which prints every registered flag with its default value and source (flag, environment or config file), and honors -h/-help.
What it is not yet
Figtree's own comparison table marks several capabilities as planned rather than shipped: struct unmarshaling and assure: tag validation (planned v2.2.0+), organizational branches (planned v2.3.0+), remote config sources (planned, no version given) and file watching (planned v2.1.1+). The current release is tagged v2.1.1 with the last commit dated 2026-10-29.
The module's go.mod directly requires five packages: golang.org/x/term, go-ini/ini (INI parsing), yaml.v3 (YAML), testify (testing) and github.com/andreimerlescu/checkfs. The README's comparison table claims "zero dependencies (core)" without defining what core excludes; the module as a whole directly requires five packages. For developers who want flag, env and config-file handling with validation built in — and who prefer the standard library's flag over pflag — figtree offers a coherent alternative to Viper.
andreimerlescu/figtree: 1 MB in 55 files, indexed Nov 9, 2026 after 0.00 hours of indexing. License: MIT. Source: https://github.com/andreimerlescu/figtree.
Ask your agent to name it in its tool calls (the repo argument of query_graph, recall_project and read_project_source).
Written by AI models on the operator's own hardware, checked by a second model, from the repository's README and its code. Back to the shelf