繁體中文

新功能

設定檔的捷徑不再被存檔動作換掉

你若把 ~/.claude/settings.json 或 ~/.codex/config.toml 放進 dotfiles(集中管理個人設定檔的儲存庫),這次不用再怕存個設定就和儲存庫走散。這些檔案常做成符號連結(指向另一個位置的捷徑檔);舊做法先寫暫存檔,再用 os.replace() 蓋過目標,原本是防止程式中斷留下半份檔案,卻會把捷徑檔換成普通檔。之後設定雖然存得進去,儲存庫那份卻不再更新。現在會先找到實際的檔案位置,並在同一處建立暫存檔,避免跨檔案系統搬動失敗。像要替住址改收件資料,從前竟把門口的指路牌拆掉;如今信件會送到收件地址。settings.json、config.toml 和 hooks.json 都共用同一套處理。

問題修復

自動修復記錄不再蓋掉剛授權的設定

剛允許一項權限,設定卻莫名消失的狀況少了一個來源。自動修復記錄會讀取 settings.json、加上一筆紀錄,再把整份檔案寫回去;若 Claude Code 正好在這個空檔寫入新的 permissions.allow,舊的記憶體副本便會把它蓋掉。空檔只有幾毫秒,才特別像難以重現的怪問題。現在這段讀取、修改、寫回會拿 flock(讓同一檔案一次只給一個人改的鎖)保護。它另用 usage-settings.lock,不與每則訊息都會寫入的狀態列鎖共用。像兩位職員同時改同一張表格,先拿走舊副本的人以前會把後來新增的核准欄蓋掉;現在先鎖住表格,輪到誰就由誰寫完。

新功能

短敏感值不再把整段紀錄塗滿

查看使用紀錄時,時間、token(AI 處理與計費的文字單位)數或錯誤碼不會再因一個過短的敏感值變成一片 [REDACTED]。程式會從逐字稿隱藏符合敏感名稱的環境變數(程式啟動前預先塞進去的隱藏值);以前它以子字串替換,若值剛好是 1,訊息裡每個 1 都被遮掉。現在少於八個字元的值不再列入遮蔽,因為這種敏感值本就不值得保護,硬遮只會毀掉周圍文字。像拿油漆滾筒遮住一個門牌號碼,從前連所有含 1 的門牌都一起刷黑;現在只處理足夠長、確實需要保密的內容。

新功能

打包的 instate 工具多了驗貨關卡

從原始碼製作 App、電腦又沒有本機 instate 專案時,下載來的 instate CLI(命令列工具,在終端機用文字指令操作的程式)現在先驗過才會被包進 App。過去 scripts/build_app.sh 會抓 release 裡的執行檔後直接使用,若下載檔被替換或損壞,沒有檢查能發現。現在會拿 scripts/instate-cli.sha256 裡固定的 SHA-256(把檔案算成一串可核對的指紋)比對;不相符便停止建置並刪掉下載檔,不留下可在下次重用的可疑檔案。這份指紋放在此儲存庫,而非和 release 檔放一起,因為能換掉執行檔的人也能一起改旁邊的指紋。下載完全失敗仍只記警告並繼續,少的是面板,不是把不可信的檔案放進去。像收貨時拿店內留存的驗貨單核對封條,而不是相信包裹自己附的紙條。

新功能

共用快取管線收好,沒用的程式碼也清掉

這次也整理了看不見的後台線路,目標是讓日後修改不必在多處重複動刀。Claude 與 Codex 的分片磁碟快取(把算過/查過的結果暫存起來直接用)原有 63 行相同程式碼,現在把序列化、分片路徑與索引、清除過期快取、讀寫資料搬到共同模組;三個載入器仍各自解析不同格式。它們共通的啟動檢查、延後寫入和結束時盡力寫入,也另收成一個模組;單一 JSON 檔的 agy 快取沒有硬塞進來。另把六個 hook(特定事件發生時被觸發的小程式)判斷與清除函式改成共用邏輯,但保留原名稱作薄包裝。整體刪掉 218 行且行為不變。還移除了死常數、兩個未用函式與 13 個孤兒翻譯鍵;檢查時沒有誤刪動態組出的鍵,也保留外部腳本仍會呼叫的函式。像整理機房線路:共用的線收到同一束,三台不同設備仍接自己的插頭。

English

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.

原始 CHANGELOG

Original changelog設定檔的捷徑不再被存檔動作換掉
**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 changelog自動修復記錄不再蓋掉剛授權的設定
**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 changelog短敏感值不再把整段紀錄塗滿
**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 changelog打包的 instate 工具多了驗貨關卡
**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 changelog共用快取管線收好,沒用的程式碼也清掉
**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.