Configuration
Configuration is, at its core, a governance problem. Who gets to decide what settings apply? What happens when two sources disagree? How do you override a default without destroying the defaults you want to keep? Most systems answer these questions badly — a flat file, some environment variables, and a prayer that nothing conflicts. This is roughly the organizational equivalent of running a government by Post-it note.
Obsidian implements a six-layer configuration hierarchy where every layer has a clear purpose, a defined scope, and explicit rules for how it interacts with every other layer. This is not accidental complexity. This is the Constitution applied to settings management: clear authority, explicit precedence, auditable decisions. Configuration is not a file. Configuration is a chain of command.
The Six Layers
Configuration flows upward through six layers, each overriding the one below it:
Layer 1: System — Organization-wide defaults at /etc/obsidian/config.yaml. Set by administrators. Contains policies that apply everywhere: approved models, blocked domains, telemetry endpoints, default secret providers. The organizational constitution for configuration — the bedrock that everything else builds on.
Layer 2: User — Personal preferences at ~/.config/obsidian/config.yaml. Not committed to version control. Your preferred editor, your default model, your credential references. The individual operating within organizational bounds — Sovereign Autonomy applied to developer preferences. You get to pick your font. You do not get to disable audit logging.
Layer 3: Environment — Environment-specific settings at .obsidian/env/{environment}.yaml. Production uses Opus with temperature 0.3, requires MFA, enables audit logging. Development uses Sonnet with temperature 0.7. The same system, different operational contexts. Because the configuration that is correct for your laptop is almost certainly wrong for production, and vice versa.
Layer 4: Project — Project-specific configuration at .obsidian/config.yaml. Agent definitions, workflow specifications, tool configurations, resource limits. This is where the system becomes specific to what you are building. Committed to version control, shared across the team. The layer where intent becomes concrete.
Layer 5: Instance — Per-deployment settings at .obsidian/instance.yaml. Auto-generated during deployment. Instance IDs, region assignments, coordinator URLs, storage locations. The physical reality of where this particular deployment lives. You did not write this file. The system wrote it for you, and the system is right.
Layer 6: Runtime — CLI flags, API calls, programmatic overrides. Highest priority, immediate effect. The escape hatch for when you need to change something now without modifying files. Power with proportional accountability — every runtime override is logged.
Merge Strategies
The interesting question is not what each layer contains — it is what happens when they disagree. Obsidian supports five merge markers that give you precise control over how configuration composes:
Deep merge (default) — objects merge recursively, arrays replace. A project timeout of 300000 overrides a user timeout of 60000, but the user’s shell preference survives because the project never mentioned it. Constraint Inheritance for configuration: what flows down can be added to, but the parent’s intent is preserved.
$override — complete replacement. When the production environment says $override: true on the LLM config, it means “forget everything below me.” No inherited temperature, no inherited timeout. Start fresh. Sometimes you need a clean slate, and the system should let you say so explicitly rather than forcing you to restate every value you want to keep.
$merge with $append/$prepend — explicit array manipulation. Add chmod 777 to the blocked commands list without restating every command already blocked. Additive security, not replacement security. Because the most dangerous configuration change is the one that accidentally removes a restriction.
$delete — surgical removal. Remove specific keys from inherited configuration without touching anything else. The scalpel, not the sledgehammer.
$remove — array element removal. Take file:write out of the allowed tools list while keeping everything else. Permission revocation without full redeclaration.
These are not clever tricks. These are governance primitives. Every organization that has ever tried to manage policy through configuration inheritance eventually needs exactly these five operations. Obsidian provides them as first-class concepts rather than waiting for you to build brittle workarounds.
Dynamic Values
Static configuration is necessary but insufficient. Obsidian supports six dynamic value resolvers that evaluate at load time:
${env:AWS_REGION} — environment variables, with optional defaults via ${env:DB_PORT:-5432}. The bridge between configuration files and deployment reality.
${secret:api-keys/anthropic} — secrets from your configured provider. Never put credentials in configuration files. This is Constitution Principle 8 — safety through boundaries, not trust .
${computed:Math.min(os.cpus().length * 2, 16)} — evaluated expressions. Scale your max concurrent Agent count to the hardware you are actually running on, not the hardware you assumed you would have.
${file:.obsidian/prompts/orchestrator.md} — file contents inlined. Keep your system prompts in version-controlled Markdown files instead of buried in YAML strings. Your future self will thank you. Your teammates will thank you sooner.
${git:shortHash} — repository metadata. Embed the commit hash in your build configuration without a separate build step.
${pkg:version} — package.json values. One source of truth for version numbers. If you are maintaining version numbers in two places, one of them is wrong.
Conditional Configuration
Because real-world configuration is never unconditional:
llm:
$if: "${env:NODE_ENV === 'production'}"
$then:
model: "claude-opus-4-20250514"
temperature: 0.3
$else:
model: "claude-sonnet-4-20250514"
temperature: 0.7
Obsidian supports $if/$then/$else blocks, $switch with multiple cases and defaults, and conditional file includes. The configuration file becomes a declaration of intent across environments, not a static snapshot of one environment’s needs. Your YAML stops describing what is and starts describing what should be, given context.
Configuration Providers
Configuration does not have to come from files. Obsidian’s provider architecture supports multiple sources through a pluggable interface:
File provider — YAML, JSON, or TOML. The default, with optional file watching for hot reload.
Environment variable provider — maps OBSIDIAN__LLM__MODEL to llm.model with automatic type coercion. The twelve-factor app pattern, properly implemented.
Consul provider — distributed configuration from HashiCorp Consul with watch support. Configuration that updates across your cluster without redeployment.
Git provider — GitOps configuration from a dedicated repository. Change configuration through pull requests with full audit trails.
HTTP provider — configuration from any HTTP endpoint with refresh intervals and retry logic.
Every provider implements the same interface: initialize, load, optionally watch, destroy. Adding a new configuration source means implementing four methods, not modifying the configuration engine. This is Constitution Principle 11 — extend without modifying core — applied to configuration itself.
Hot Reload
Configuration changes should not require restarts. Obsidian’s hot reload system watches for changes across all providers, validates the new configuration against the schema before applying it, and emits events that components can subscribe to for graceful reconfiguration.
hotReload:
enabled: true
debounceMs: 1000
validateBeforeApply: true
rollbackOnError: true
If the new configuration fails validation, it is rejected. If applying the new configuration throws an error, the system rolls back. Configuration changes are deployments — they deserve the same safety guarantees. The Warden applies the same scrutiny to configuration changes that it applies to any other system state mutation. Because a misconfigured system is an unconstitutional system, and the difference between a bug and a misconfiguration is purely academic when production is down.
Validation
Every configuration value is validated against a typed schema with constraints. The Warden does not accept malformed configuration any more than it accepts Constitutional Compliance violations — the principle is the same. Invalid input is rejected at the boundary, not discovered at runtime.
# Validate configuration
obs config validate
# Show resolved configuration (all layers merged)
obs config show --resolved
# Show where each value comes from
obs config show --provenance
The --provenance flag deserves special mention: it shows you exactly which layer provided each value in your final configuration. When something is wrong, you do not grep through six files — you ask the system to tell you who is responsible. The obs interface for configuration follows the same principle as everything else in Obsidian: if it is not observable, it does not exist.
Why Configuration Is Worth Getting Right
Here is the thing that most teams discover too late: configuration is policy. Every YAML key is a decision. Every override is a governance act. Every merge conflict between layers is a jurisdictional dispute. You can pretend configuration is a boring operational detail, or you can acknowledge that the way you manage settings reflects the way you manage everything else.
Obsidian chooses to take configuration seriously — six layers with explicit precedence, five merge strategies with clear semantics, six dynamic resolvers, conditional blocks, hot reload with validation, and full provenance tracking. Not because configuration needs to be complicated, but because Systemic Integrity demands that every part of the system, including its settings, operates within constitutional bounds.
Autonomy under constraint. Even for YAML.