Search/Build a Command-Line Tool That Never Loses User Data or Runs Unapproved Code

Build a Command-Line Tool That Never Loses User Data or Runs Unapproved Code

Use when an AI builds or changes a CLI that already has users. It keeps flags and config compatible, stops instead of destroying work, runs project hooks only after approval, traces every subprocess, and shows local results before network data.

To use this AI rule
## Keep config files and command-line flags compatible as the tool grows Once a tool has real users, balance clean design against compatibility. A redesign is not, by itself, a reason to break people. ### Justify every external break Changing an external interface needs a concrete user-facing improvement, not just cleanup. Prefer a deprecation warning and a migration path over a silent break. ### Name the protected surface Decide which interfaces are contracts (the config-file format, the command-line flags and arguments) and which may change freely (internal APIs, log locations, human-readable output formatting). Guard the first set; keep the second flexible so refactors stay cheap. ### Bound the support window State a minimum supported runtime or toolchain version and advance it on a predictable maintenance cadence rather than ad hoc. ## Use one word for each idea across docs, help text, and comments Pick one term per concept and use it identically in documentation, help text, and code comments. Drifting vocabulary quietly confuses both users and maintainers. ### Write the glossary down Keep a short list that fixes each word's meaning and the near-synonyms to avoid. Distinguish look-alike concepts explicitly, such as the original checkout versus a secondary one, or the destination of an operation versus the workspace it runs in. ### Never overload a load-bearing word If a word already names one concept, do not reuse it for another; choose a different word for the second even when the reuse reads naturally in a single sentence. ## Update help text with each behavior change and test that docs match Every behavior change carries its documentation change in the same edit. The test to apply: does the help text still describe what the code actually does? ### Keep one primary source per fact For generated docs, edit the source of truth (the code-embedded help and command definitions) and regenerate the rendered mirrors; never hand-edit a generated file. Where one directory holds both generated mirrors and hand-authored pages, confirm which role a file plays before editing it. ### Enforce sync with a test A doc-sync test that compares rendered output against the source catches drift mechanically. Editing help text also changes rendered snapshots, so regenerate and review those in the same change to keep the test green. ## Stop with an error instead of destroying user files without consent Treat user data as sacred. A command that fails while preserving work beats one that "succeeds" by silently destroying it. ### Prefer failure over silent loss If an operation might destroy untracked files, uncommitted changes, or other user data, stop with an error instead of proceeding. ### Require explicit consent to destroy A destructive action, such as force-removing data or overwriting, runs only when the user explicitly asked for it. Never delete or overwrite as a side effect of an unrelated operation; make cleanup a separate step the user chooses. ### Favor the failing variant, mind the check-to-use gap When two forms of an operation exist, pick the one that fails on conflict over the one that forces through. Be conservative when time passes between a safety check and the action it guards: state can change in that window, so do not force through during cleanup. ## Send every subprocess through one wrapper that logs and times it Send every external command through a single execution wrapper instead of calling the process API directly. One path yields consistent debug logging and timing for free, plus one place to change behavior. ### One wrapper, one emitter Keep the trace grammar in exactly one component and make it the only writer of the underlying log calls, so a subprocess is either traced through that guard or produces no record at all. ### New spawn sites opt in A few spawn shapes cannot fit the wrapper and construct the trace record by hand: start it just before spawn, complete it on success, fail it on error. Make the guard a must-use value that trips an assertion if dropped unresolved, so a forgotten completion fails a test rather than going silently untraced. Detached background children and interactive helpers are the documented untraced exceptions. ### Scrub inherited discovery variables Some environment variables are resolved before a child consults its working directory, so an inherited value can silently override the directory you chose. Any spawn site that relocates a user command into a directory the tool selected must clear those variables; children running in the user's own context keep the inherited context, absolutized. ## Decide on exit codes and JSON output, not on message text Prefer exit codes, machine-readable status, and structured formats over parsing human-readable messages, which break on locale, version, and wording changes. ### Choose the structured channel Read porcelain or JSON output and numeric status flags instead of default human output. Where a command encodes meaning in its exit code, branch on the code, not on message text. ### Document unavoidable fragility When no structured alternative exists and you must parse text, mark the fragility inline at the parse site so the next reader knows it is a known risk, not a stable contract. ## Show local results first and stream network data in afterward Responsiveness is a feature: paint from local data first, then let slower detail arrive behind it. ### Never let the network gate the first frame Touch the network only when the user's request needs it. Every command should render its first output from local data and stream network-derived detail progressively; a command that cannot draw its first frame until a remote call returns is the failure mode, worst on a fresh setup. A synchronous hot path, such as a shell prompt, must not reach the network at all, even progressively. ### Prefer user-bounded contexts for live remote data Stream command output line by line rather than buffering it. An interactive picker is the most forgiving home for network work: it paints immediately, and if the user acts before a slow call returns, the request is simply abandoned. A run-to-completion command cannot finish until every call returns, so fetch live remote data in the picker, progressively. ## Stop every loop when the user interrupts a running command When a child process dies from a signal (interrupt or terminate), every loop in the foreground execution path must abort rather than advance to the next iteration. ### Read the structured signal channel Detect signal-derived exits from the structured signal field, never by sniffing for an exit code at or above 128 or parsing error text. Propagate a canonical interrupted-exit result and break the loop. ### Check before any error-swallowing branch Do the interrupt check before any failure-handling strategy runs, so a "warn and continue" policy cannot swallow an interrupt. Any new code that loops over child processes performs this check per iteration; otherwise one interrupt against a multi-step operation charges through the remaining steps. ## Run project-defined hooks and aliases only after approval Project-defined commands (lifecycle hooks, aliases, execute-bodies pulled from project config) are arbitrary code shipped in a repository the user may have just cloned. They run only after an approval system clears them. ### The gate is the whole security boundary Never build a code path that runs project-defined commands without that gate. A "we already validated the operation, so run its hooks too" shortcut turns every command that touches project config into remote code execution; the gate is the only thing between cloning a repository and a hook running a piped-from-the-internet script. ### Select the approved set once, immutably Gate the command set into an immutable approved plan at the decision point and let the executor consume only that value, holding nothing it could re-derive from, so re-selecting an unapproved command becomes a compile error rather than a review check. A context that cannot prompt consults approval state read-only and runs only the already-approved subset. ## Never merge with a failing coverage check without the user's approval Never merge a change with a failing coverage gate without explicit user approval, even when the platform marks that gate "not required" because it still blocks in practice. ### Know how the signal is reported Coverage tools may post results as check runs rather than commit statuses, read through different APIs; a combined-status query can miss a check run entirely, and the result may land minutes after the job finishes. Poll the correct API before concluding a change is green. ### Fix the gap, do not wave it through On a coverage failure, investigate and close the gap: add tests, or delete unused code, including special-case handlers where falling through to a general path suffices. If you believe it is a false positive, ask the user before merging rather than deciding unilaterally. Run coverage with the same feature flags the CI pipeline uses, so you cannot dismiss a failure by claiming a feature was off. ## Reuse existing libraries and fix warnings instead of silencing them Check existing dependencies before hand-rolling a utility. A proven library for path normalization, shell escaping, coloring, or template-variable detection beats an ad hoc reimplementation that mishandles the edge cases. ### Do not paper over warnings Never silence a warning by blanket-suppressing it. Delete the dead code, or leave a dated, explained marker tying it to specific upcoming work. ### Document complex systems in place A non-obvious system (a state machine, cached state, cross-module coordination, subtle invalidation) carries a module-level spec covering purpose, key decisions, contracts, and invariants; keep it current. ### Keep tests out of library code Do not add test-only convenience methods to library types; tests call the real API or define their own helpers. For multiline literals, prefer real embedded newlines over line-continuation escapes that silently strip whitespace. ## Add context to errors and never turn them into crashes Carry context with every error. Use explicit business-logic errors for expected failure states (a dirty workspace, a missing branch, an invalid state) and context-wrapping for I/O and external-command failures. ### Never swallow a failure In a function that returns an error type, never use panic-on-absent or unwrap-style calls that turn a recoverable failure into a crash; propagate with the error operator, raise an explicit error, or return one. Every wrapped error should tell the reader which operation failed and why. ## Migrate old config keys instead of silently dropping them Keep all config-compatibility handling in one layer: rewrite deprecated patterns into canonical form before the parser sees them. Never silently drop an old config key, because that is a silent behavior change; migrate it instead. ### One predicate for detect and migrate Let one idempotent function both rewrite a deprecated pattern and report what it changed, so detection and migration share a single predicate and cannot drift. Run detection by applying those same functions to a scratch copy, so a warning fires exactly when a materializing update would change the file; pin that equivalence with a test. ### Classify each rule's timing Decide per rule when a rewrite applies: on every load, only on an explicit update, silently with no warning, or as a pending default a later release will adopt. Leave a value that cannot be rewritten safely (malformed, or a destination key already occupied) untouched, and let the parser's own type error carry the message. ## Name accessors so the prefix shows their cost, and decide where cached values live Encode intent in accessor names so a caller can predict cost and failure mode from the prefix alone. ### Let the prefix carry the contract Adopt a consistent scheme: a bare noun returns a value with no side effects and may cache; a set-prefixed name writes state and can error; a require-prefixed name errors when the value is absent; a fetch-prefixed name performs network I/O; a load-prefixed name performs file I/O. Avoid a redundant get-prefix where a bare noun reads better. ### Decide where cached state lives Share read-only cached values through a reference-counted cache that cloning shares. Choose the tier by cost and heat: cheap-and-hot values get an in-memory get-or-create; expensive values persist to disk; values that are both keep an in-memory front over a persistent back. Document what is and is not cached, and the invariant for refreshing it after a mutation.