One Schema to Rule Them All: The Config v2 Rewrite The akm project has consolidated its config layer into a single Zod schema in the 0.8.0 release, replacing approximately 1,400 lines of legacy parser code. The rewrite eliminates silent failures from unknown keys, corrupt JSON, and missing files by enforcing strict validation on typo-prone fields like registries and profiles. A new config field now requires only a one-line schema addition, with TypeScript types automatically inferred from the schema. This is part sixteen in a series about managing the growing pile of skills, scripts, and context that AI coding agents depend on. The 0.8.0 release notes https://dev.to/itlackey/akm-080-cli-redesign-task-assets-and-belief-aware-memory-335a cover the storage and pipeline changes that shipped alongside this rewrite; Part thirteen https://dev.to/itlackey/from-30-minutes-to-8-how-llm-mode-reflect-works-5epl covers how the new profiles.improve config drives the improve pipeline. Config files are where projects go to accumulate technical debt quietly. Each new feature gets a new key. Each new key gets a new parser. Each parser has slightly different error handling, slightly different defaults, and slightly different ideas about what "invalid" means. Nobody notices until a user files an issue that says "I had a typo in my config and akm just silently used defaults for three weeks." That was the state of akm's config layer going into 0.8.0. The v1 config had three top-level blocks that grew independently over two years: llm. for LLM connection settings, agent. for agent process settings, and llm.features. boolean flags gating per-feature LLM calls. The features block was nested under llm for historical reasons even though many features used the agent, not the LLM. The agent's per-process map lived under agent.processes , while LLM-gated features used llm.features.index.metadata enhance style dotted paths. Each block had its own parser function. parseLlmConfig , parseEmbeddingConfig , parseIndexConfig , and a dozen more. The comment at the top of the new config-schema.ts is blunt about it: the Zod schema "replaces the ~1.4k LOC of legacy per-shape parsers." The problems that accumulated in that ~1.4k LOC: Unknown keys were silently accepted. If you wrote llm.temperaure typo , the parser ignored it and fell back to the default temperature. No warning. You tuned a key that did nothing. Bad JSON was masked. The config loader caught JSON parse errors and fell back to DEFAULT CONFIG — the compiled-in defaults. Your entire config file could be corrupt and akm would start without complaint, using defaults across the board. Missing files fell back to defaults. Same behavior. A missing config file and a present-but-corrupt one looked identical at runtime. Adding a field meant adding a parser. Want a new boolean flag under a feature? Find the right parser function, add the extraction logic, add the type declaration, add the hint string, add the test. The cost of a new field was not one line — it was a small PR touching four or five places. The 0.8.0 rewrite consolidates all of that into src/core/config-schema.ts : a single Zod schema that is the source of truth for the on-disk shape. Zod handles the parse, transform, and validate steps that were previously scattered across ~1.4k LOC of hand-written code. A new config field is a one-line schema addition. Type inference means the TypeScript types for AkmConfig are derived from the schema automatically — no parallel maintenance between the schema and the type declarations. The schema design makes deliberate tradeoffs between strictness and resilience: The top-level object uses .passthrough so unknown future keys round-trip intact. If a user upgrades and then downgrades, keys added by the newer version survive without triggering errors on the older version. sanitizeConfigForWrite decides what to strip on write. Nested sub-objects use .catch undefined for field-level shape errors so that a typo in one field does not destroy an otherwise valid config. This preserves the legacy parser's warn-and-ignore semantics for individual fields while still catching structural problems. .strict walls gate the records that are most typo-prone: registries , sources , and profiles. sub-shapes. A typo in a profile name or a source type now produces a validation error at load time. Two cases are hard-rejected by superRefine rather than silently dropped: the old stashes key replaced by sources and a legacy source type that had been removed. Both have explicit migration paths — silently ignoring them would mask user data loss. The new loader changed three behaviors that were causing silent failures in the field. Unknown keys now error at the profile level. A typo in profiles.llm.my-profile is caught at load time rather than ignored. The error message names the unexpected key and points at the profile block. Bad JSON now throws. If config.json is not valid JSON, akm throws a ConfigError with the file path and the parse error. No fallback to defaults. The user finds out immediately. Missing files stay missing. A missing config file is a different situation from a corrupt one, and akm treats them differently now. First run with no config: akm setup or an explicit akm config set creates the file. A missing file during a subsequent run is an error, not a silent fallback. With a Zod schema as the source of truth, generating a JSON schema for editor autocompletion is a natural output. The schemas/akm-config.json file is generated from the Zod schema and checked in. A CI drift test fails if the checked-in file is out of sync with the schema source — there is no manual step to remember when adding a field. Point your editor at the schema and you get field completion and inline documentation in config.json : { "$schema": "https://itlackey.github.io/akm/schemas/akm-config.0.8.0.json", "configVersion": "0.8.0" } The $schema key is optional. VSCode and other JSON Schema-aware editors pick it up automatically for field completion and inline docs. The 0.8.0 shape replaces the scattered llm. , agent. , and llm.features. blocks with a unified profiles tree and first-class feature sections. | Old location | New location | |---|---| llm.endpoint , llm.model , llm.apiKey | profiles.llm.