tr
TryGhost
GitHub
Ghost
Workspace
GitHub
CI Pipeline Executions
Current execution
Succeeded
claude/config-readonly-guard
Runs and Agents
Timeline
Beta
Configuration
Self-Healing CI
Project Graph
Circular Dependencies
Enterprise
Resource Usage
Analysis
Conformance
Enterprise
Runs
Current run
Succeeded
Tasks
Resource Usage
Insights
Compare tasks
Analytics
Sign in
Toggle sidebar
Added a read-only guard that makes config writes throw in dev and CI no ref Deep-freezing config makes it immutable but not loud. Ghost's .js files and its CommonJS dependencies are sloppy-mode, where a write to a frozen object is dropped without an error, so a caller that mutates config fails silently and no test can see it. `node --use-strict` does not help: it makes only the entry point strict, and a `require()`d CommonJS module keeps its own strictness, with or without tsx. Enforcing the freeze that way would take 'use strict' directives across 1330 files, 6 of which have one today. A proxy trap throws whatever mode the caller is in. So `development` and anything starting with `test` - the environments this repo runs itself, the same set that validates strictly - now get a proxy whose set, delete, defineProperty and setPrototypeOf traps throw and name the key path. Production keeps the frozen object: it is the cheaper read, and the guard's value is in catching writes before they ship rather than after. GHOST_CONFIG_GUARD overrides either way. The two cannot be combined. A proxy over a deep-frozen target cannot return a wrapped child from its get trap, because the invariant for non-writable, non-configurable properties forbids handing back anything other than the target's own value, so a guarded tree is left unfrozen and relies on the traps. Children are wrapped lazily and memoised per underlying object, which keeps `===` stable between reads of the same subtree and terminates on cycles. Symbols and functions are handed back unwrapped, or iterators, spreads and array methods break. Only plain objects and arrays are wrapped. Reads cost more - 4.5ns to 21ns for a scalar, 120ns to 2.4µs for a spread of a config object - which is the other reason production keeps the freeze. It makes no measurable difference to the test suite: 8.50s against 9.04s over 9340 tests. One change outside the guard: config.set() now clones overrides with cloneDeep rather than structuredClone, because under the guard the value may be a proxy and structuredClone throws on those. The tests drive a deliberately sloppy-mode fixture, because a test file is no use here - vitest compiles those to ESM, which is always strict, so a write would throw with or without the guard. They stub GHOST_CONFIG_GUARD rather than inheriting it, so they do not depend on what a developer has exported. Verified against the unit, integration and e2e suites on both MySQL and SQLite, and a development boot. The guard found exactly one real violation while being built, fixed in the preceding commit: knex-migrator mutating the database config it was handed.
nx run ghost-monorepo:lint:boundaries
⌘K
Succeeded
nx run ghost-monorepo:lint:boundaries
Click to copy
Linux
4 CPU cores
read-write
access token used
55a9de8b
31330
GitHub
Ghost
Workspace