Release notes

FEATURE

Saving settings no longer replaces your config shortcut

If you keep ~/.claude/settings.json or ~/.codex/config.toml in a dotfiles repository, saving a setting will no longer quietly split it from that repository. These files are often symlinks, shortcut files that point elsewhere. The old safe-save routine wrote a temporary file and used os.replace() to prevent a crash from leaving half a file, but that replacement swapped out the shortcut itself for a plain file. Settings still saved, while the repository copy stopped changing. It now resolves the real target first and creates the temporary file beside it, avoiding a failed move across filesystems. It was like replacing an address sign instead of delivering mail to the address; now the delivery reaches the real place. settings.json, config.toml, and hooks.json use the same helper.

BUGFIX

Self-heal logging no longer overwrites fresh permissions

A newly allowed permission should no longer disappear for no obvious reason. The self-heal log reads settings.json, adds an entry, and writes back the full file. If Claude Code wrote a new permissions.allow entry in the tiny gap, the logger's older in-memory copy could overwrite it. The gap lasts milliseconds, which made the bug look mysterious. That read-modify-write sequence now uses flock, a lock that lets only one writer change the file at a time. It has its own usage-settings.lock, separate from the status-line lock that protects a different file written on every message. Picture two clerks editing one form: the clerk holding an old copy used to erase a newly approved field. Now the form is locked until each clerk finishes their turn.

FEATURE

Short sensitive values no longer blot out whole transcripts

When you inspect a transcript, timestamps, token counts, and error codes should no longer turn into [REDACTED] because of a very short secret. The program hides sensitive-named environment variables, hidden values supplied before a program starts, by replacing matching substrings. Previously, if a value happened to be 1, every 1 in the message was hidden. Values shorter than eight characters are now skipped: such a sensitive value is not worth protecting, while masking it destroys surrounding text. It was like using a paint roller to hide one house number and blacking out every address containing 1; now the roller is reserved for values long enough to need it.

FEATURE

Bundled instate tool now gets a verification check

When building the App from source without a local instate project, the downloaded instate CLI now has to pass inspection before it is bundled. scripts/build_app.sh previously downloaded a release binary and used it as-is, so a substituted or corrupted file had no check to catch it. The build now compares it with the fixed SHA-256 in scripts/instate-cli.sha256, a fingerprint calculated from a file. A mismatch stops the build and deletes the download, so no suspect binary waits around for the next build. The fingerprint lives in this repository rather than beside the release asset: someone able to replace the binary could replace its neighboring checksum too. A download that fails completely still logs a warning and continues; that means a missing panel, not an untrusted binary. It is like checking a package seal against an inspection sheet kept in the store, not a note inside the package.

FEATURE

Shared cache plumbing was gathered up and dead code removed

Some invisible back-room wiring was tidied so future changes do not require the same edit in several places. Claude and Codex's sharded disk caches, stored results reused directly, had 63 identical lines. Serialization, shard paths and index, stale-cache removal, and payload read/write now live in a common module, while the three loaders still parse their own unrelated formats. Their shared startup check, delayed flush, and best-effort flush at shutdown moved to another module; agy's single-JSON cache was not forced into that shape. Six hook check-and-strip functions now share logic while retaining their original names as thin wrappers. This removes 218 lines with no behavior change. Dead constants, two unused functions, and 13 orphaned translation keys also went away, without deleting runtime-built keys or a function called by an outside script. Like sorting a server room's cables: shared wires go in one bundle, while three different devices keep their own plugs.

Original changelog

Original changelogSaving settings no longer replaces your config shortcut
**Saving a setting turned a symlinked config file into a regular one.** Every settings write goes through `_atomic_write_text()`, which writes a temporary file and then `os.replace()`s it over the target — the standard way to make sure a crash mid-write cannot leave half a file behind. `os.replace()` does not follow symlinks, though, so for anyone who manages `~/.claude/settings.json` or `~/.codex/config.toml` from a dotfiles repository, the symlink was silently replaced by a plain file: the repository copy stopped receiving updates, nothing warned, and the divergence only showed up the next time those dotfiles were deployed somewhere else. The target is now resolved to its real path before the replace, and the temporary file is created next to that real path rather than next to the link — putting it beside the link would move the rename across filesystems, which fails outright with `Invalid cross-device link` and would have traded one silent breakage for a louder one. The atomic write itself is unchanged. `statusline_settings.py` had grown its own copy of this sequence and now calls the shared helper, so `settings.json`, `config.toml` and `hooks.json` are all covered by one implementation.
Original changelogSelf-heal logging no longer overwrites fresh permissions
**The self-heal log could discard settings Claude Code had just written.** `_append_self_heal_log()` reads `settings.json`, appends an entry and writes the whole file back. Claude Code writes the same file, and anything it stored between that read and that write — a newly granted `permissions.allow` entry, for instance — was overwritten by the older copy still held in memory. The window is milliseconds wide, which is exactly why it would have been diagnosed as a mystery rather than as a race. The read-modify-write is now held under a `flock`, reusing the lock helper the status line already uses. It takes its own `usage-settings.lock` rather than sharing the status line's `usage-status.lock`: that one guards a different file written on every message, and sharing it would serialise two unrelated things for no benefit.
Original changelogShort sensitive values no longer blot out whole transcripts
**A short secret redacted every occurrence of its characters.** `_redact_environment_values()` masks sensitive environment values out of transcripts by substring replacement. Nothing bounded how short a value could be, so an environment variable matching the sensitive-name pattern whose value happened to be `1` turned every `1` in the message into `[REDACTED]` — timestamps, token counts, error codes. Values shorter than eight characters are now skipped; a secret that short is not one worth protecting, and treating it as one destroys the surrounding text.
Original changelogBundled instate tool now gets a verification check
**The vendored instate CLI was bundled and executed without being verified.** `scripts/build_app.sh` downloads the binary from a release when the local `instate` project is not present, then packages it into the `.app` as-is. There was nothing to detect a substituted or corrupted download. The build now pins the expected SHA-256 in `scripts/instate-cli.sha256` and refuses to continue when the downloaded binary does not match, deleting it rather than leaving a suspect file behind for the next run to reuse. The fingerprint is committed to this repository rather than published beside the release asset on purpose: a checksum stored next to the binary can be rewritten by whoever rewrites the binary, which verifies nothing. A download that fails outright still logs a warning and continues — that is a missing panel, not a compromised one.
Original changelogShared cache plumbing was gathered up and dead code removed
**Three passages of duplicated code were extracted.** The sharded disk caches for Claude and Codex history held sixty-three identical lines, and `codex_disk_cache.py` reached across to import `history_disk_cache.py`'s private serialisers and re-export them under its own `__all__` — a detour that made the real owner of that code impossible to see. Serialisation, shard paths and index, stale-cache removal and the payload read/write now live in `disk_cache_common.py`. The disk-cache lifecycle shared verbatim by all three loaders — the one-time seed check, the throttled flush and the best-effort flush on termination — moved to `disk_cache_lifecycle.py`. The loaders themselves are still three separate modules and stay that way; they parse three unrelated formats, and only the cache plumbing around them was ever the same. In `session_hooks.py`, three pairs of `_is_X_entry`/`_strip_X_hooks` differing only in a marker constant were parameterised, with the six original names kept as thin wrappers. `agy_disk_cache.py` was deliberately left out: it is a single JSON file with no sharding, and forcing it into the shared shape would have meant changing its behaviour to fit. Net 218 lines removed, with no behavioural change.
**Dead constants, two unused functions and thirteen orphaned translation keys were removed.** The keys came from renames and from the AI-updates feature moving to its own repository. Finding them took more care than the count suggests: a plain text search over the source reported 159 orphans, of which 146 were false. Keys are frequently assembled at runtime rather than written out — `discussion.html` builds `discussion_status_round1_running` by lowercasing a `SessionStatus` value, and `ui/html_report.py` prepends `report_` to every key it looks up, so four live `report_wrapped_beast_*` entries appear nowhere in the source under their full names. `agy_loader.recent_input_output_tokens()` looks equally dead from inside this repository and was kept: its only caller is a script outside it.