msu-statusline-install
Wires the MSU status line into the user's own settings. Claude Code
reads statusLine from settings.json rather than from plugin config, so a plugin
cannot install one — this skill writes it on the user's behalf.
What lands where, all under CONFIG = ${CLAUDE_CONFIG_DIR:-$HOME/.claude}:
| Path |
What it is |
$CONFIG/msu-statusline.sh |
copy of ${CLAUDE_PLUGIN_ROOT}/scripts/launcher.sh; what settings.json points at |
$CONFIG/msu-statusline.prev |
the status-line command that was configured before, verbatim |
$CONFIG/msu-statusline.conf |
the settings msu-statusline-config edits |
$CONFIG/msu-statusline.cache |
the last notice read, plus .lock, .attempted and .failures beside it |
settings.json points at the copy rather than into the plugin because the plugin
cache is versioned — a path into it would break on the next claude plugin update.
The launcher resolves the most recently installed copy itself, on every render, so an
update to the body of the status line is picked up with nothing to re-run. The launcher
copy is the exception: it is a file, not a resolution, and claude plugin update does
not touch it. Re-running this install is what refreshes it, and step 3 does that
unconditionally.
Install
Everything below writes to $CONFIG, so assign it once in whatever shell you use:
CONFIG=${CLAUDE_CONFIG_DIR:-$HOME/.claude}
S=$CONFIG/settings.json
Two things hold for every write to settings.json in either direction. Keep its file
mode — the obvious atomic idiom, mktemp then mv, silently replaces a 0644 file
with a 0600 one, and this is the file holding the user's permissions and hooks. Clone
the mode by making the temporary file a copy of the original, which cp -p does and
mktemp cannot:
S=$CONFIG/settings.json
cp -p "$S" "$S.new" && jq '<the edit>' "$S" > "$S.new" && mv -f "$S.new" "$S" \
|| { rm -f "$S.new"; exit 1; }
cp -p first, then the redirect: > truncates the copy and leaves its mode alone, and
mv over the original is atomic, so a jq that fails writes nothing. On a machine with
no settings file the cp fails and there is no mode to keep — write the file directly
there. A failed edit stops the install; cleanup must not turn its exit status into
success. Check that bash, jq and curl are available before making changes.
And leave a statusLine sibling you did not put there alone, padding being the one
that exists today.
Read $CONFIG/settings.json. Create $CONFIG if it is not there, and treat a
missing or empty file as {} — a first install has neither, and the JSON below is
then the whole file rather than a patch. Initialise a missing or empty file to {}
before running the jq edits; jq on empty input produces no document and exits
successfully. Stop on malformed JSON, a non-object root or statusLine, or a
non-string command, without replacing the file.
Edit the file as JSON, never as text: it holds the user's permissions and hooks, and a bad
patch costs them more than this feature. Parsing and re-serialising reflows the
file — every value survives, the formatting may not, and that is fine.
Preserve an existing status line. If .statusLine.command is set and does
not already mention msu-statusline, write it to $CONFIG/msu-statusline.prev
— the decoded value of that JSON string, the shell command itself, because the
launcher runs it with bash -c. Add no trailing newline: removal puts this file
straight back into a JSON string, and a stray \n there is a command that no longer
matches what the user had. If there is no existing command, remove any stale .prev
from an earlier install: replaying it would resurrect a line the user has removed.
jq -e 'type == "object"
and (.statusLine == null or (.statusLine | type == "object"))
and (.statusLine.command == null or (.statusLine.command | type == "string"))' \
"$S" >/dev/null || exit 1
if jq -e '.statusLine.command // "" | contains("msu-statusline")' "$S" >/dev/null; then
: # Repair: keep the existing backup.
elif jq -e '.statusLine.command // "" | length > 0' "$S" >/dev/null; then
jq -j '.statusLine.command' "$S" > "$CONFIG/msu-statusline.prev" || exit 1
else
rm -f "$CONFIG/msu-statusline.prev" || exit 1
fi
-j, not -r. Both decode the string; -r adds the newline this step exists to
avoid, and it is the spelling that comes to hand first.
If .prev already exists and the current command is not ours, the user changed
their status line since the last install: overwrite it, because the newer one is
what they expect back.
If the current command does mention msu-statusline, this is a repair or a
reinstall: leave .prev exactly as it is. Writing the launcher's own command into
.prev makes the launcher run itself, find .prev non-empty, and run itself again —
unbounded recursion, two processes per level, on every status-line redraw.
Match on msu-statusline, exactly as msu-statusline-uninstall does, and not on
msu-statusline.sh: a command pointing straight into the plugin at
…/msu-statusline/<version>/scripts/launcher.sh is this plugin's too, and the
stricter spelling does not see it. The launcher refuses to replay a .prev naming
itself, so a mismatch here costs the user's own status line rather than the machine —
but it costs it silently, which is why the two skills have to match on the same thing.
Copy the launcher: ${CLAUDE_PLUGIN_ROOT}/scripts/launcher.sh →
$CONFIG/msu-statusline.sh, then make it executable.
Point settings.json at it. Set .statusLine.type and .statusLine.command
and nothing else — assigning a whole new statusLine object would silently drop a
sibling the user had set, such as padding. The two fields, not the block below as a
template; on a machine with no settings file the block is a fragment, not a document.
Store the command literally, ${CLAUDE_CONFIG_DIR:-...} and all. It is written
that way so it resolves when the status line runs; let a shell expand it while you
build the string and you bake in today's absolute path instead.
"statusLine": {
"type": "command",
"command": "bash \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msu-statusline.sh\""
}
No refreshInterval. Claude Code redraws the line on every session event and on
that timer, so the timer earns nothing at the default ten-minute colour cycle — the
events are already far more frequent. It is only worth adding for an ICON_CYCLE
short enough that an idle session would visibly freeze the colour, under a minute or
so, and then it costs a full re-run of this command on every tick, including any
status line being wrapped.
Create $CONFIG/msu-statusline.conf if it is absent — never overwrite one that
exists, it is the user's configuration. Ask the script for its contents rather than
transcribing them from anywhere:
if [ ! -e "$CONFIG/msu-statusline.conf" ]; then
bash "$CONFIG/msu-statusline.sh" --config > "$CONFIG/msu-statusline.conf.new" \
&& mv "$CONFIG/msu-statusline.conf.new" "$CONFIG/msu-statusline.conf" \
|| { rm -f "$CONFIG/msu-statusline.conf.new"; exit 1; }
fi
The guard is in the block, not only in the sentence above it. Every other step here
ships a block meant to be run as written, so one that skipped the check would take
the user's settings on the next reinstall — the exact thing the sentence forbids.
Redirecting straight onto the real name would leave an empty conf behind if the
command failed — and step 5's own "never overwrite one that exists" would then
protect that empty file from every future install. --config fails with a non-zero
status and nothing on stdout when the plugin cannot be resolved, which is exactly the
case this guards: no conf is written. Report the error and repair plugin resolution
before continuing to step 6; the launcher prints its diagnostic on stderr.
With no conf to read, that prints exactly the defaults in KEY=value form, which is
the format the file takes. Do not assemble the list by reading the script: the keys
are named once inside it and every other list — including any in this file — would be
a second source that goes stale.
Show the real line, which also proves the whole chain works. Feed it a session
JSON object rather than /dev/null: a wrapped status line usually reads one, and on
empty input it prints nothing and makes this step look like a failure.
jq -n --arg dir "$PWD" '{model:{display_name:"Opus"},workspace:{current_dir:$dir}}' \
| bash "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msu-statusline.sh"
Expect the MSU line when NOTICE=on and a notice is cached, and above it the previous
status line if step 2 saved one. A fetch without a cache waits up to three seconds;
after a failure, the next eligible retry also waits until there is a cache. With a
cache, later polls run in the background. jq --arg keeps the sample JSON valid
even when the working directory contains quotes or backslashes.
⚠ MSU statusline: plugin not found means the launcher cannot see the plugin. It
resolves the newest $CONFIG/plugins/cache/*/msu-statusline/*/, which matches nothing
when the plugin is a checkout rather than an installed copy. Point
MSU_STATUSLINE_ROOT at the plugin directory in the environment that actually runs
the status line, or install the plugin properly. A shell-only override proves only
that shell's render; it does not configure future Claude Code sessions.
An empty render alone does not prove a wiring failure. NOTICE=off, or a cold
cache after a failed fetch, legitimately prints no MSU line. Check --config, the
cache and .cache.failures before claiming success or a broken launcher. Consult
the troubleshooting reference below if the expected line is missing.
Then report what actually rendered. Settings reload automatically and appear at the
next interaction with Claude Code; a new session is a fallback if the current one does
not update, not a requirement. If step 2 saved a command, its output renders above MSU.
This reload behaviour is documented in the
Claude Code status-line guide.
Removing it
Taking the line back out — restoring the status line that was there before, deleting
what the steps above wrote, and uninstalling the plugin — is
msu-statusline-uninstall. The order it insists on is real: uninstalling the plugin
first takes that skill away with it.
When something looks wrong
A symptom and what it means — a blank line, a stale notice, a red warning, a link that
will not open — is in
references/troubleshooting.md. Read it when the
request is a symptom rather than an install.
Segments and how often the board is polled belong to msu-statusline-config.
1---2name: msu-statusline-install3description: Claude Code only — a status line is a Claude Code concept and this skill does nothing on another CLI. Puts the newest MSU Builder notice in the Claude Code status line, and repairs it. Use when the user asks to install, enable, set up, re-install or repair the MSU status line, wants MSU notices or announcements shown under the prompt or in the terminal, or reports that the MSU status line is blank, stale, or gone after an update. Wraps a status line that is already configured rather than replacing it. Taking it back out is `msu-statusline-uninstall`.4---56# msu-statusline-install78Wires the MSU status line into the user's own settings. Claude Code9reads `statusLine` from `settings.json` rather than from plugin config, so a plugin10cannot install one — this skill writes it on the user's behalf.1112What lands where, all under `CONFIG` = `${CLAUDE_CONFIG_DIR:-$HOME/.claude}`:1314| Path | What it is |15|---|---|16| `$CONFIG/msu-statusline.sh` | copy of `${CLAUDE_PLUGIN_ROOT}/scripts/launcher.sh`; what `settings.json` points at |17| `$CONFIG/msu-statusline.prev` | the status-line command that was configured before, verbatim |18| `$CONFIG/msu-statusline.conf` | the settings `msu-statusline-config` edits |19| `$CONFIG/msu-statusline.cache` | the last notice read, plus `.lock`, `.attempted` and `.failures` beside it |2021`settings.json` points at the copy rather than into the plugin because the plugin22cache is versioned — a path into it would break on the next `claude plugin update`.23The launcher resolves the most recently installed copy itself, on every render, so an24update to the body of the status line is picked up with nothing to re-run. The launcher25*copy* is the exception: it is a file, not a resolution, and `claude plugin update` does26not touch it. Re-running this install is what refreshes it, and step 3 does that27unconditionally.2829## Install3031Everything below writes to `$CONFIG`, so assign it once in whatever shell you use:3233```bash34CONFIG=${CLAUDE_CONFIG_DIR:-$HOME/.claude}35S=$CONFIG/settings.json36```3738Two things hold for every write to `settings.json` in either direction. **Keep its file39mode** — the obvious atomic idiom, `mktemp` then `mv`, silently replaces a 0644 file40with a 0600 one, and this is the file holding the user's permissions and hooks. Clone41the mode by making the temporary file a copy of the original, which `cp -p` does and42`mktemp` cannot:4344```bash45S=$CONFIG/settings.json46cp -p "$S" "$S.new" && jq '<the edit>' "$S" > "$S.new" && mv -f "$S.new" "$S" \47 || { rm -f "$S.new"; exit 1; }48```4950`cp -p` first, then the redirect: `>` truncates the copy and leaves its mode alone, and51`mv` over the original is atomic, so a `jq` that fails writes nothing. On a machine with52no settings file the `cp` fails and there is no mode to keep — write the file directly53there. A failed edit stops the install; cleanup must not turn its exit status into54success. Check that `bash`, `jq` and `curl` are available before making changes.5556And **leave a `statusLine` sibling you did not put there alone**, `padding` being the one57that exists today.58591. **Read `$CONFIG/settings.json`.** Create `$CONFIG` if it is not there, and treat a60 missing or empty file as `{}` — a first install has neither, and the JSON below is61 then the whole file rather than a patch. Initialise a missing or empty file to `{}`62 before running the `jq` edits; `jq` on empty input produces no document and exits63 successfully. Stop on malformed JSON, a non-object root or `statusLine`, or a64 non-string command, without replacing the file.65 Edit the file as JSON, never as text: it holds the user's permissions and hooks, and a bad66 patch costs them more than this feature. Parsing and re-serialising reflows the67 file — every value survives, the formatting may not, and that is fine.682. **Preserve an existing status line.** If `.statusLine.command` is set and does69 **not** already mention `msu-statusline`, write it to `$CONFIG/msu-statusline.prev`70 — the decoded value of that JSON string, the shell command itself, because the71 launcher runs it with `bash -c`. Add no trailing newline: removal puts this file72 straight back into a JSON string, and a stray `\n` there is a command that no longer73 matches what the user had. If there is no existing command, remove any stale `.prev`74 from an earlier install: replaying it would resurrect a line the user has removed.7576 ```bash77 jq -e 'type == "object"78 and (.statusLine == null or (.statusLine | type == "object"))79 and (.statusLine.command == null or (.statusLine.command | type == "string"))' \80 "$S" >/dev/null || exit 181 if jq -e '.statusLine.command // "" | contains("msu-statusline")' "$S" >/dev/null; then82 : # Repair: keep the existing backup.83 elif jq -e '.statusLine.command // "" | length > 0' "$S" >/dev/null; then84 jq -j '.statusLine.command' "$S" > "$CONFIG/msu-statusline.prev" || exit 185 else86 rm -f "$CONFIG/msu-statusline.prev" || exit 187 fi88 ```8990 `-j`, not `-r`. Both decode the string; `-r` adds the newline this step exists to91 avoid, and it is the spelling that comes to hand first.9293 If `.prev` already exists and the current command is *not* ours, the user changed94 their status line since the last install: overwrite it, because the newer one is95 what they expect back.9697 **If the current command does mention `msu-statusline`, this is a repair or a98 reinstall: leave `.prev` exactly as it is.** Writing the launcher's own command into99 `.prev` makes the launcher run itself, find `.prev` non-empty, and run itself again —100 unbounded recursion, two processes per level, on every status-line redraw.101102 Match on `msu-statusline`, exactly as `msu-statusline-uninstall` does, and not on103 `msu-statusline.sh`: a command pointing straight into the plugin at104 `…/msu-statusline/<version>/scripts/launcher.sh` is this plugin's too, and the105 stricter spelling does not see it. The launcher refuses to replay a `.prev` naming106 itself, so a mismatch here costs the user's own status line rather than the machine —107 but it costs it silently, which is why the two skills have to match on the same thing.1083. **Copy the launcher**: `${CLAUDE_PLUGIN_ROOT}/scripts/launcher.sh` →109 `$CONFIG/msu-statusline.sh`, then make it executable.1104. **Point `settings.json` at it.** Set `.statusLine.type` and `.statusLine.command`111 and nothing else — assigning a whole new `statusLine` object would silently drop a112 sibling the user had set, such as `padding`. The two fields, not the block below as a113 template; on a machine with no settings file the block is a fragment, not a document.114115 Store the command **literally**, `${CLAUDE_CONFIG_DIR:-...}` and all. It is written116 that way so it resolves when the status line runs; let a shell expand it while you117 build the string and you bake in today's absolute path instead.118119 ```json120 "statusLine": {121 "type": "command",122 "command": "bash \"${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msu-statusline.sh\""123 }124 ```125126 No `refreshInterval`. Claude Code redraws the line on every session event *and* on127 that timer, so the timer earns nothing at the default ten-minute colour cycle — the128 events are already far more frequent. It is only worth adding for an `ICON_CYCLE`129 short enough that an idle session would visibly freeze the colour, under a minute or130 so, and then it costs a full re-run of this command on every tick, including any131 status line being wrapped.1321335. **Create `$CONFIG/msu-statusline.conf` if it is absent** — never overwrite one that134 exists, it is the user's configuration. Ask the script for its contents rather than135 transcribing them from anywhere:136137 ```bash138 if [ ! -e "$CONFIG/msu-statusline.conf" ]; then139 bash "$CONFIG/msu-statusline.sh" --config > "$CONFIG/msu-statusline.conf.new" \140 && mv "$CONFIG/msu-statusline.conf.new" "$CONFIG/msu-statusline.conf" \141 || { rm -f "$CONFIG/msu-statusline.conf.new"; exit 1; }142 fi143 ```144145 The guard is in the block, not only in the sentence above it. Every other step here146 ships a block meant to be run as written, so one that skipped the check would take147 the user's settings on the next reinstall — the exact thing the sentence forbids.148149 Redirecting straight onto the real name would leave an empty conf behind if the150 command failed — and step 5's own "never overwrite one that exists" would then151 protect that empty file from every future install. `--config` fails with a non-zero152 status and nothing on stdout when the plugin cannot be resolved, which is exactly the153 case this guards: no conf is written. Report the error and repair plugin resolution154 before continuing to step 6; the launcher prints its diagnostic on stderr.155156 With no conf to read, that prints exactly the defaults in `KEY=value` form, which is157 the format the file takes. Do not assemble the list by reading the script: the keys158 are named once inside it and every other list — including any in this file — would be159 a second source that goes stale.1606. **Show the real line**, which also proves the whole chain works. Feed it a session161 JSON object rather than `/dev/null`: a wrapped status line usually reads one, and on162 empty input it prints nothing and makes this step look like a failure.163164 ```bash165 jq -n --arg dir "$PWD" '{model:{display_name:"Opus"},workspace:{current_dir:$dir}}' \166 | bash "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/msu-statusline.sh"167 ```168169 Expect the MSU line when `NOTICE=on` and a notice is cached, and above it the previous170 status line if step 2 saved one. A fetch without a cache waits up to three seconds;171 after a failure, the next eligible retry also waits until there is a cache. With a172 cache, later polls run in the background. `jq --arg` keeps the sample JSON valid173 even when the working directory contains quotes or backslashes.174175 **`⚠ MSU statusline: plugin not found` means the launcher cannot see the plugin.** It176 resolves the newest `$CONFIG/plugins/cache/*/msu-statusline/*/`, which matches nothing177 when the plugin is a checkout rather than an installed copy. Point178 `MSU_STATUSLINE_ROOT` at the plugin directory in the environment that actually runs179 the status line, or install the plugin properly. A shell-only override proves only180 that shell's render; it does not configure future Claude Code sessions.181182 **An empty render alone does not prove a wiring failure.** `NOTICE=off`, or a cold183 cache after a failed fetch, legitimately prints no MSU line. Check `--config`, the184 cache and `.cache.failures` before claiming success or a broken launcher. Consult185 the troubleshooting reference below if the expected line is missing.186187Then report what actually rendered. Settings reload automatically and appear at the188next interaction with Claude Code; a new session is a fallback if the current one does189not update, not a requirement. If step 2 saved a command, its output renders above MSU.190This reload behaviour is documented in the191[Claude Code status-line guide](https://code.claude.com/docs/en/statusline).192193## Removing it194195Taking the line back out — restoring the status line that was there before, deleting196what the steps above wrote, and uninstalling the plugin — is197`msu-statusline-uninstall`. The order it insists on is real: uninstalling the plugin198first takes that skill away with it.199200## When something looks wrong201202A symptom and what it means — a blank line, a stale notice, a red warning, a link that203will not open — is in204[`references/troubleshooting.md`](references/troubleshooting.md). Read it when the205request is a symptom rather than an install.206207Segments and how often the board is polled belong to `msu-statusline-config`.