Repository navigation
fix: report Uninstall and Refresh Interval failures when Claude's settings.json can't be changed - #805
Merged
sirmalloc merged 6 commits intoNov 6, 2026
Conversation
uninstallStatusLine caught a load failure (invalid JSON such as a trailing comma, or a permission error), printed a warning and returned normally. The TUI's uninstall action treats a normal return as "Claude settings removed", so it went on to run `npm uninstall -g` or `bun remove -g`, switched the menu to not installed and flashed "✓ Uninstalled from Claude Code and removed global package". Nothing had been removed from settings.json: once the file was fixed, statusLine still ran the global binary that had just been uninstalled, which leaves a blank status line, and the managed --hook hooks and the installation metadata were still there. The settings-only options reported "✓ Uninstalled from Claude Code" the same way. The TUI only offers Uninstall when settings.json was readable at startup, so this takes the file breaking while the TUI is open, for example a hand edit saved with a syntax error in another window. uninstallStatusLine now throws on that failure, as installStatusLine already does, with the same message naming the file and the reason from a loader the two share. The TUI's existing failure branch then runs: it shows "✗ Uninstall failed", keeps the installed state and skips the global package removal. settings.json, the metadata and the hooks are left as they were.
setRefreshInterval returned normally when Claude's settings.json could not be read or parsed, and when it had no statusLine, without writing anything. Configure Status Line sets the new value before the save and only puts the old one back if the save rejects, so in both cases it flashed "✓ Refresh interval updated" and kept showing the new value, while Claude Code went on using the old one. As with Uninstall, the menu is only enabled when settings.json was readable and had our statusLine at startup, so this takes the file changing while the TUI is open: a hand edit that leaves invalid JSON, or another tool replacing the file. setRefreshInterval now throws in both cases, the read failure with the message install and uninstall use. The TUI's existing failure branch then shows "✗ Failed to save refresh interval" and keeps the value that is still in effect. The file is left unchanged.
uninstallStatusLine ignored any failure to remove the managed --hook hooks. When statusLine was already gone from Claude's settings.json, so nothing was written before that step, and the file couldn't be written (read-only, for example), the hooks stayed in place and the uninstall returned normally. The TUI then ran `npm uninstall -g` or `bun remove -g` and flashed success, leaving hooks that run the binary it had just removed. The installation metadata was cleared before the hooks were tried, so it was gone as well. The hook cleanup failure now rejects the uninstall, and the hooks are removed before the metadata is cleared, so the TUI's existing failure branch shows "✗ Uninstall failed", skips the global package removal and leaves settings.json and the metadata as they were. removeManagedHooks no longer rewrites settings.json when it holds none of our hooks. An uninstall with neither statusLine nor managed hooks in the file still goes ahead even when the file can't be written, since nothing in it would be left pointing at the binary. It also stops reformatting the file and refreshing its .bak copy for no change.
… read Removing the managed hooks assumed every hook event held a list of objects, each with a list of hooks. A hand-edited settings.json can break that, for example a Stop hook written as an object, a null entry, or "hooks" set to a string. The cleanup then threw a TypeError, even when none of the hooks were ccstatusline's, and since the previous change that error rejects the uninstall. statusLine had already been removed in an earlier write, so the TUI showed "Uninstall failed" with statusLine gone, and every retry failed the same way. Entries without the expected shape are now left as they are, since they can't be ours. Uninstall also removes statusLine and the managed hooks in one write, so a failure to save leaves both in place, and the .bak copy holds the file as it was before the uninstall rather than the halfway state between the two writes.
# Conflicts: # src/utils/hooks.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
When the TUI can't change Claude Code's settings.json, it now reports a failure instead of claiming success:
npm uninstall -g/bun remove -gran anyway and the TUI flashed "✓ Uninstalled from Claude Code and removed global package", whilestatusLine, the managed--hookhooks and the installation metadata were all still there. Uninstall also fails now whenstatusLineis already gone but the managed--hookhooks are still in a settings.json that can't be written, for example a read-only one. That failure used to be ignored, so the package was removed and the hooks were left running it. A hook entry in a shape Claude Code doesn't expect, such as one written by hand, is left as it is and no longer stops the cleanup.statusLine, Configure Status Line shows "✗ Failed to save refresh interval" and keeps the value that is still in effect. Before, it flashed "✓ Refresh interval updated" and showed the new value although nothing was written.Why (evidence)
Both writers caught the failure and returned normally:
uninstallStatusLineprinted "Warning: Could not read existing Claude settings." and returned. It also ignored any failure to remove the managed hooks, which is the only write to settings.json whenstatusLineis already gone.setRefreshIntervalreturned without a word when the read failed and when there was nostatusLine.The TUI treats a normal return as success. It only takes its existing failure paths ("✗ Uninstall failed", "✗ Failed to save refresh interval") when the call rejects, so these cases never reached them.
The TUI reads settings.json once at startup, and only offers Uninstall and Configure Status Line if the file parsed and had our
statusLine. So hitting this takes the file changing while the TUI is open, for example a hand edit saved with a trailing comma in another window. Reproduced onmain, as an Ink test and in the demo below:npm uninstall -g ccstatuslineruns.statusLine.commandis stillccstatusline, the binary that was just removed, so Claude Code shows a blank status line. The--hookhooks point at it too.refreshInterval: 10; the file given a trailing comma, or rewritten withoutstatusLine): "✓ Refresh interval updated" and "(15s)", while settings.json still says 10.installStatusLinealready refuses to touch an unreadable settings.json (#627), and the TUI reports that as "Install Failed". Uninstall and Refresh Interval now do the same.How
claude-settings.ts, the read guardinstallStatusLinehad inline moves intoloadClaudeSettingsForUpdate(retry). It throws "Could not read existing Claude settings at (). The file was left unchanged; fix or remove it, then ." Install, uninstall andsetRefreshIntervaluse it; install's message is unchanged.uninstallStatusLinethrows before removing anything, so the metadata and hooks stay in place. It now removesstatusLineand the managed hooks in one write, before it clears the metadata, so a failure to save rejects with both still in the file and the metadata kept. With neither in the file it doesn't write at all, so Uninstall still goes ahead whenstatusLineis already gone and no managed hooks are left, even if the file can't be written, since nothing would be left pointing at the binary. The.bakcopy now holds the file as it was before the uninstall.hooksisn't a list. Before, such an entry threw a TypeError, which uninstall ignored.setRefreshIntervalalso throws when there is nostatusLine, including when clearing the interval: the TUI's idea that ccstatusline is installed is out of date in that state..catchputs the previous value back.Demo
The TUI runs in the top of a tmux split. The shell below adds a trailing comma to
~/.claude/settings.jsonmid-session. npm is a stub that only recordsnpm uninstall -g ccstatusline.Uninstall. Before: the warning bleeds in, "✓ Uninstalled from Claude Code and removed global package", and the stub log shows npm ran. After: "✗ Uninstall failed", it stays on the uninstall screen, and npm never runs.
Powerline
Before:
After:
Plain
Before:
After:
Refresh Interval. Before: "✓ Refresh interval updated" and "(15s)" on reopening, while the file still says 10. After: "✗ Failed to save refresh interval", "(10s)" on reopening, and the file is unchanged.
Powerline
Before:
After:
Plain
Before:
After:
Testing
mainas of fix(powerline): keep the Custom theme when Powerline is enabled again #793 (a0bedc0): it merges cleanly.bun run lintis clean, andbun testgives 4155 pass, 0 fail (4147 onmain). The built CLI under Bun 1.4.4 and Node 26.13.0 agrees across runtimes and matchesmainin plain and Powerline mode, and the TUI opens and exits cleanly under both. It adds no JavaScript or Node API newer than Node 14, the build's target.mainand pass here:claude-settings.test.ts: 3 changed, 4 new. Uninstall rejects and leaves the file and metadata alone, both when the file can't be parsed and when managed hooks are left in a read-only file. With neitherstatusLinenor managed hooks in a read-only file, Uninstall resolves, clears the metadata and doesn't try to rewrite the file. Uninstall keeps hook entries it doesn't recognize, and its.bakcopy is the file from before the uninstall.setRefreshIntervalrejects on an unreadable file and on a missingstatusLine.App.test.ts: 4 new Ink tests. Uninstall shows "✗ Uninstall failed", never calls the global uninstall, and leaves the file, the metadata and the installed state alone. That holds with a settings.json that no longer parses, and with a read-only one that has lost itsstatusLinebut kept the managed hooks. Refresh Interval with an unparseable file or nostatusLineshows the failure flash, keeps "(10s)" and leaves the file unchanged.main. The same 11 tests fail on both (spying onnode:child_processexecSyncisn't allowed under Node ESM).hooks.test.ts, whose source changed, passes 7 / 7 on both.