Repository navigation
fix: open Configure Status Line before an install, and sync hooks only from the config the status line loads - #807
Merged
sirmalloc merged 7 commits intoNov 6, 2026
Conversation
Configure Status Line was disabled with "(install first)" unless Claude Code's statusLine command was one of the exact commands the installer writes, with padding 0. A wrapper script, `bunx ccstatusline`, `npx ccstatusline@latest` without -y, or any other padding all counted as not installed. That menu is the only place in the TUI to edit the Git, Custom Command and Terminal Width cache TTLs. They are ccstatusline's own settings, saved to its settings.json and read on every render however Claude Code runs it. With such a status line they couldn't be changed, for example to turn on custom command caching, which is off by default. The menu now always opens. Only its Refresh Interval row needs the install: Claude Code keeps that value in its statusLine setting, which ccstatusline edits only for a status line it recognizes as its own. Until then the row is disabled with "(install first)"; an older Claude Code still shows its version requirement instead. The main menu's description of the item says which part needs the install.
Saving a config rewrote Claude Code's ccstatusline-managed hooks from that config's widgets, whichever config it was. The installed status line command loads one config: the path after its --config, or the default config without one. Saving any other config, such as one opened with `ccstatusline --config <file>` to try something out, still rewrote the hooks to match it: - If the status line shows Skills and the saved config doesn't, the Skills hooks were removed and the status line's Skills widget stopped updating. - In the other direction, hooks were added that make Claude Code run `<command> --hook` on every prompt and Skill call, for a status line that doesn't need them. Either way the TUI only said "Configuration saved". syncWidgetHooks now works out which config the status line command loads and leaves the hooks as they are when that isn't the config being saved. getStatusLineConfigPath splits the command into words the way a POSIX shell passes them on (quotes and backslash escapes; on Windows only double quotes group), expands a leading ~ and $HOME, and resolves the path as the --config argument is resolved at startup, through resolveConfigPath, which initConfigPath now uses too. A command without --config, a wrapper script included, is taken to load the default config. Installing still syncs the hooks, since the command it writes names the config the TUI has open.
# Conflicts: # src/utils/config.ts
# Conflicts: # docs/DEVELOPMENT.md # docs/USAGE.md
# Conflicts: # src/utils/config.ts
Node 14, the build's target, has no logical assignment operators, and the build passes them through unchanged.
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
Two fixes to how the TUI uses Claude Code's status line command.
~/.claude/ccstatusline-wrapper.sh,bunx ccstatusline,npx ccstatusline@latestwithout-y, and anypaddingother than 0. The menu now opens, so Git Cache TTL, Custom Command Cache TTL and Terminal Width Cache TTL can be edited. Only its Refresh Interval row still reads "(install first)" until ccstatusline is installed, because that value is written into Claude Code's ownstatusLinesetting.--config, or the default config without one. Two kinds of save no longer remove hooks that status line needs, or add hooks it doesn't need:--config;--config.Why
Configure Status Line locked
buildMainMenuItemsdisabled the item wheneverisInstalled()was false.isInstalled()passes only in two cases:statusLine.commandis exactlynpx -y ccstatusline@latest,bunx -y ccstatusline@latestorccstatusline, optionally followed by--config …;ccstatusline.tspath, as a local development command does.paddingmust also be 0 or unset.The gate dates from when the menu held only the refresh interval: it was hidden without an install, and version pinning turned that into the greyed-out "(install first)" item. The three cache TTL rows were added to the menu later, and it is the only place in the TUI that edits them. They are ccstatusline's own settings, saved in its settings.json and read on every render, however Claude Code runs it.
customCommandCacheTtlSecondsdefaults to 0, which runs the command on every render, so with such a status line custom command caching couldn't be turned on from the TUI.I wrote each command to a scratch
CLAUDE_CONFIG_DIR/settings.jsonand called the realisInstalled()andbuildMainMenuItems(). Onmain:On this branch the first four print
disabled=false.isInstalled()itself is unchanged.Hooks rewritten from the wrong config
Every save (
Ctrl+Sand Save & Exit) runssaveSettings, which callssyncWidgetHookswith the config being saved. That function removed every ccstatusline-managed hook and added back the ones that config's widgets need, pointing at<statusLine.command> --hook. Its only check was that the command containsccstatusline. It never looked at which config the command loads.A script with a scratch HOME and
CLAUDE_CONFIG_DIRcallsinitConfigPath()and thensaveSettings(await loadSettings()), the calls the TUI makes when it opens a config and whenCtrl+Ssaves it. Onmain(<home>is the scratch home):--hookhandler is the only thing that records skill use, so that status line's Skills widget shows none from then on.ccstatusline --config alt.json --hookand waits for it. The status line it serves doesn't show Skills.In both cases the TUI only said "✓ Configuration saved".
On this branch:
--config ~/alt.json, written with a tilde, and alt.json is the config being saved.How
Configure Status Line (
fix(tui): open Configure Status Line before ccstatusline is installed):MainMenu.tsx: the item is always enabled. Its description now reads "Set ccstatusline's cache TTLs, and Claude Code's refresh interval once ccstatusline is installed".RefreshIntervalMenu.tsx: a newisClaudeInstalledprop, also a trailing parameter ofbuildConfigureStatusLineItems. It's optional and defaults totrue, the only state in which the menu used to open. When it's false, the Refresh Interval row is disabled with "(install first)". On an older Claude Code, "(requires Claude Code >=2.1.97)" still shows instead, since installing wouldn't help. With the row disabled, the cursor starts on Git Cache TTL.App.tsxpasses its install state to the menu.setRefreshIntervalwritesstatusLine.refreshIntervalinto Claude Code's settings.json. Uninstall is likewise offered only for a command the TUI recognizes as its own, and Install replaces any other command with its own.docs/USAGE.mddescribes the split.Hooks (
fix(hooks): sync widget hooks only from the config the status line loads):getStatusLineConfigPath(command)inclaude-settings.ts, next to the code that writes--configinto the command:quotePathIfNeeded. On Windows only double quotes group words, and backslashes stay as path separators.--config, asparseConfigArgdoes, and expands a leading~and$HOME.resolveConfigPath, whichinitConfigPathnow uses too.--confighas no path, and the default config path when there's no--config.syncWidgetHooks: after the existing "containsccstatusline" check, it returns without writing settings.json whengetStatusLineConfigPath(statusCommand)isn'tgetConfigPath(). Installing still syncs the hooks, because the command it writes loadsgetConfigPath().docs/DEVELOPMENT.mddescribes the rule.Left as is:
statusLine.refreshIntervalhas to be set in Claude Code's settings.json by hand, as before.--config. A wrapper that passes its arguments on (ccstatusline "$@") handles that. A script that names its config inside itself gets hooks only when the default config is saved.--configinside a nested shell string (bash -c '… ccstatusline --config …') isn't seen, so that command counts as loading the default config.--configresolves against the TUI's working directory.HOMEor globs, aren't evaluated, so the path doesn't match and the hooks stay as they are.~or$HOMEis expanded even inside single quotes, where the shell leaves it alone. So a config whose path contains a literal$HOMEnever matches, and neither Install nor a save syncs its hooks.--config, the "Config:" line under the TUI's title shows which file is open.Demo
Configure Status Line with a wrapper script
Claude Code's status line is
~/.claude/ccstatusline-wrapper.sh. The TUI runs in the top of a tmux split, and the shell below shows Claude's settings.json. Before: Configure Status Line reads "(install first)", and Down from Global Overrides jumps to Export Config. After: Down lands on Configure Status Line. Inside, Refresh Interval reads "(install first)" and the cache TTLs are editable. The Custom Command Cache TTL is set to 5s and saved, and the shell shows it in ccstatusline's settings.json.Powerline
Before:
After:
Plain
Before:
After:
Saving a config the status line doesn't load
Claude Code's status line runs
ccstatusline --config ~/work.json, a config with a Skills widget, and has the managed Skills hooks. The TUI edits the default config, which has no Skills widget. The shell below greps Claude's settings.json for its commands before and afterCtrl+S. Before: the save removes both Skills hooks. After: they stay.Powerline
Before:
After:
Plain
Before:
After:
Testing
mainas of fix(powerline): keep the Custom theme when Powerline is enabled again #793 (a0bedc0): it merges after one conflict inconfig.tsnext to fix(config): don't migrate a version-less settings.json with later settings as v1 #779's migration notice, resolved by keeping both.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.main. The 3 inclaude-settings.test.tsfail there becausegetStatusLineConfigPathdoesn't exist, so that file doesn't load:App.test.ts, new Ink test with a wrapper-script status line and the realisInstalled():statusLineis unchanged.On
main, Down from Global Overrides lands on Export Config.App.test.ts, changed: the main menu structure test now expects Configure Status Line enabled when not installed, and the install row's selection index to be 8 instead of 7.RefreshIntervalMenu.test.ts, 1 new: without an install, Refresh Interval is disabled with "(install first)" and the three TTL rows stay enabled.hooks.test.ts, 3 new:--configadds no hooks;--config ~/'my configs'/statusline.json, saving the default config adds nothing, and saving that file adds the hooks.claude-settings.test.ts, 3 new forgetStatusLineConfigPath:~,$HOME, escaped spaces, a missing path;buildStatusLineCommandwrites for a plain path and for paths with a space, a single quote, and",$, backticks and backslashes;main: 164 pass and the same 11 fail. The 11 are the existinggetClaudeCodeVersion/isClaudeCodeVersionAtLeasttests whosechild_processspies don't take under Node's ESM. All new tests pass under Node.