Design robust config and state file handling with safe defaults and crash recovery.
Copy the install command and let the AI configure it · recommended for beginners
Please install the "config-state-patterns" skill from askskill: 1. Download https://raw.githubusercontent.com/microsoft/amplifier-bundle-skills/main/skills/config-state-patterns/SKILL.md 2. Save it as ~/.claude/skills/config-state-patterns/SKILL.md 3. Reload skills and tell me it's ready
Design a config and state file strategy for a desktop app. Separate file locations for config, state, and secrets; support merging defaults with user config; state writes must be atomic to avoid corruption after crashes. Provide directory conventions, example data structures, and the write flow.
A clear config/state/secrets storage pattern with paths, merge rules, and atomic write logic.
I am building a CLI tool. Help me define persistence rules with safe defaults, user overrides, runtime state caching, and separate storage for sensitive tokens. Explain what should go into config files, state files, and secrets files.
A CLI-focused file responsibility guide explaining where each data type belongs and why.
Give me a crash-safe state file write strategy for a local app. It should write to a temporary file first, then atomically replace the real file, while considering concurrent writes, permissions, and recovery strategy. Pseudocode is preferred.
A practical atomic write workflow with guidance for concurrency, permissions, and recovery.
Problem: Your tool has user-configurable settings (host, port, auth mode) and runtime state (which sessions are active, device heartbeats) that must persist across restarts and handle concurrent reads/writes safely.
Approach: Separate config from state. Use a defaults-merge-overlay pattern with known-keys-only filtering for settings. Use atomic writes (write-to-tmp-then-os.replace) for state. Use asyncio locks for concurrent access. Follow XDG-conventional paths.
Pattern proven in production across multiple Python CLI tools and web services.
The settings file might be from an older version (missing new keys) or a newer version (has keys we don't understand). The load_settings() pattern handles both:
def load_settings() -> dict:
result = copy.deepcopy(DEFAULT_SETTINGS) # start with ALL defaults
try:
text = SETTINGS_PATH.read_text()
data = json.loads(text)
for key in DEFAULT_SETTINGS: # only copy KNOWN keys
if key in data:
result[key] = data[key]
except (FileNotFoundError, json.JSONDecodeError):
pass # corrupt/missing = use defaults
return result
The critical detail: iteration is over DEFAULT_SETTINGS keys, not over the file's keys. Unknown keys in the file are silently ignored. This prevents config drift when a user downgrades or when settings are synced between versions.
The same principle applies when saving:
def save_settings(data: dict) -> None:
merged = copy.deepcopy(DEFAULT_SETTINGS)
for key in DEFAULT_SETTINGS:
if key in data:
merged[key] = data[key]
SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
SETTINGS_PATH.write_text(json.dumps(merged, indent=2) + "\n")
And on patch (partial update):
def patch_settings(patch: dict) -> dict:
current = load_settings()
for key in DEFAULT_SETTINGS:
if key in patch:
current[key] = patch[key]
os.replaceState files can be read by other processes at any time. A naive write_text() can produce a half-written file if the process crashes mid-write.
The simple pattern:
def save_state(state: dict) -> None:
STATE_DIR.mkdir(parents=True, exist_ok=True)
tmp = Path(str(STATE_PATH) + ".tmp")
tmp.write_text(json.dumps(state, indent=2))
os.replace(tmp, STATE_PATH) # atomic on POSIX
For extra safety (no predictable tmp path, proper cleanup on error), use tempfile.mkstemp:
def _write_instance(self, instance_id: str, data: dict) -> None:
path = self._instance_path(instance_id)
path.parent.mkdir(parents=True, exist_ok=True)
content = json.dumps(data, ensure_ascii=False, default=str)
fd, tmp_path = tempfile.mkstemp(dir=path.parent, suffix=".tmp")
try:
os.write(fd, content.encode("utf-8"))
os.close(fd)
Path(tmp_path).replace(path)
except BaseException:
with contextlib.suppress(OSError):
os.close(fd)
Path(tmp_path).unlink(missing_ok=True)
raise
When state is accessed from a poll loop and from API handlers simultaneously, a module-level asyncio lock serializes access:
state_lock: asyncio.Lock = asyncio.Lock()
async def read_state() -> dict:
async with state_lock:
return load_state()
async def write_state(state: dict) -> None:
async with state_lock:
save_state(state)
For threading contexts, use threading.Lock per instance with a defaultdict:
self._locks: defaultdict[str, threading.Lock] = defaultdict(threading.Lock)
# Usage — every mutation acquires the per-instance lock:
…
Research, plan, and execute large code changes with parallel PR-producing agents.
Analyze images, extract text, and answer visual questions with LLM vision models.
Get skeptical, practical guidance on architecture, legacy refactors, and tooling decisions.
Design auth and TLS patterns for smooth local use and secure remote access.
Design CLI tools with simple installs, command routing, and layered config.
Design extensible systems with runtime plugin discovery, registries, and validation.
Implement filesystem-based inter-process communication without requiring a message broker.
Verify and create standard community and governance files for new projects.
Safely orchestrate Docker tasks and build reproducible development stack environments.
Helps developers add, review, and export VS Code configuration policies.
Set up Claude Code with best practices for reliable, efficient development.
Learn production-grade Kubernetes patterns, access control, scaling, and debugging workflows.