Skip to content

Commit 87c4edb

Browse files
committed
fix: parse claude statusline input without eval
1 parent 50b68c0 commit 87c4edb

2 files changed

Lines changed: 98 additions & 24 deletions

File tree

.claude/statusline.sh

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -56,39 +56,81 @@ format_reset() {
5656
}
5757

5858
# --- Parse stdin JSON ---
59-
eval "$(echo "$input" | jq -r '
60-
"MODEL=" + (.model.display_name // "Unknown" | @sh),
61-
"CTX_SIZE=" + (.context_window.context_window_size // empty | tostring),
62-
"CTX_USED_PCT=" + (.context_window.used_percentage // 0 | tostring),
63-
"CTX_INPUT=" + ((.context_window.current_usage.input_tokens // 0) | tostring),
64-
"CTX_CACHE_CREATE=" + ((.context_window.current_usage.cache_creation_input_tokens // 0) | tostring),
65-
"CTX_CACHE_READ=" + ((.context_window.current_usage.cache_read_input_tokens // 0) | tostring),
66-
"CTX_HAS_USAGE=" + (if .context_window.current_usage then "1" else "0" end),
67-
"CTX_USED_TOKENS=" + ((.context_window.current_usage.input_tokens // 0) + (.context_window.current_usage.cache_creation_input_tokens // 0) + (.context_window.current_usage.cache_read_input_tokens // 0) | tostring),
68-
"CTX_TOTAL_TOKENS=" + (.context_window.context_window_size // empty | tostring),
69-
"TOTAL_INPUT=" + (.context_window.total_input_tokens // 0 | tostring),
70-
"TOTAL_OUTPUT=" + (.context_window.total_output_tokens // 0 | tostring),
71-
"LINES_ADD=" + (.cost.total_lines_added // 0 | tostring),
72-
"LINES_DEL=" + (.cost.total_lines_removed // 0 | tostring),
73-
"FIVE_PCT=" + (.rate_limits.five_hour.used_percentage // empty | floor | tostring),
74-
"FIVE_RESET_EPOCH=" + (.rate_limits.five_hour.resets_at // 0 | tostring),
75-
"SEVEN_PCT=" + (.rate_limits.seven_day.used_percentage // empty | floor | tostring),
76-
"SEVEN_RESET_EPOCH=" + (.rate_limits.seven_day.resets_at // 0 | tostring),
77-
"EFFORT=" + (.effort.level // "" | @sh)
78-
' 2>/dev/null)"
59+
MODEL="Unknown"
60+
CTX_SIZE=0
61+
CTX_USED_PCT=0
62+
CTX_INPUT=0
63+
CTX_CACHE_CREATE=0
64+
CTX_CACHE_READ=0
65+
CTX_HAS_USAGE=0
66+
CTX_USED_TOKENS=0
67+
CTX_TOTAL_TOKENS=0
68+
TOTAL_INPUT=0
69+
TOTAL_OUTPUT=0
70+
LINES_ADD=0
71+
LINES_DEL=0
72+
FIVE_PCT=0
73+
FIVE_RESET_EPOCH=0
74+
SEVEN_PCT=0
75+
SEVEN_RESET_EPOCH=0
76+
CWD="."
77+
EFFORT=""
78+
79+
if command -v jq >/dev/null 2>&1; then
80+
parsed=$(
81+
printf '%s\n' "$input" | jq -r '
82+
def int_or_zero:
83+
if type == "number" then floor
84+
elif type == "string" and test("^[0-9]+(\\.[0-9]+)?$") then tonumber | floor
85+
else 0
86+
end;
87+
def string_or($default):
88+
if type == "string" and length > 0 then . else $default end;
89+
90+
[
91+
(.model.display_name | string_or("Unknown")),
92+
(.context_window.context_window_size | int_or_zero),
93+
(.context_window.used_percentage | int_or_zero),
94+
(.context_window.current_usage.input_tokens | int_or_zero),
95+
(.context_window.current_usage.cache_creation_input_tokens | int_or_zero),
96+
(.context_window.current_usage.cache_read_input_tokens | int_or_zero),
97+
(if .context_window.current_usage then 1 else 0 end),
98+
((.context_window.current_usage.input_tokens | int_or_zero) + (.context_window.current_usage.cache_creation_input_tokens | int_or_zero) + (.context_window.current_usage.cache_read_input_tokens | int_or_zero)),
99+
(.context_window.context_window_size | int_or_zero),
100+
(.context_window.total_input_tokens | int_or_zero),
101+
(.context_window.total_output_tokens | int_or_zero),
102+
(.cost.total_lines_added | int_or_zero),
103+
(.cost.total_lines_removed | int_or_zero),
104+
(.rate_limits.five_hour.used_percentage | int_or_zero),
105+
(.rate_limits.five_hour.resets_at | int_or_zero),
106+
(.rate_limits.seven_day.used_percentage | int_or_zero),
107+
(.rate_limits.seven_day.resets_at | int_or_zero),
108+
(.workspace.current_dir | string_or(".")),
109+
(.effort.level // "")
110+
] | @tsv
111+
' 2>/dev/null
112+
)
113+
if [ -n "$parsed" ]; then
114+
IFS=$'\t' read -r \
115+
MODEL CTX_SIZE CTX_USED_PCT CTX_INPUT CTX_CACHE_CREATE CTX_CACHE_READ \
116+
CTX_HAS_USAGE CTX_USED_TOKENS CTX_TOTAL_TOKENS TOTAL_INPUT TOTAL_OUTPUT \
117+
LINES_ADD LINES_DEL FIVE_PCT FIVE_RESET_EPOCH SEVEN_PCT \
118+
SEVEN_RESET_EPOCH CWD EFFORT <<< "$parsed"
119+
fi
120+
fi
79121

80122
# --- Per-model default context length ---
81-
if [ -z "$CTX_SIZE" ] || [ "$CTX_SIZE" = "null" ] || [ "$CTX_SIZE" = "0" ]; then
123+
if [ "$CTX_SIZE" -eq 0 ]; then
82124
case "$MODEL" in
83125
*qwen*|*Qwen*) CTX_SIZE=256000 ;;
84126
esac
85127
CTX_TOTAL_TOKENS=$CTX_SIZE
86128
fi
87129

88-
if [ "$CTX_HAS_USAGE" = "1" ]; then
130+
if [ "$CTX_HAS_USAGE" = "1" ] && [ "$CTX_SIZE" -gt 0 ]; then
89131
CTX_PCT=$(( (CTX_INPUT + CTX_CACHE_CREATE + CTX_CACHE_READ) * 100 / CTX_SIZE ))
90132
CTX_NUM_STR="$(format_tokens $CTX_USED_TOKENS)/$(format_tokens $CTX_TOTAL_TOKENS)"
91-
elif [ -n "$CTX_SIZE" ] && [ "$CTX_SIZE" != "null" ] && [ "$CTX_SIZE" != "0" ]; then
133+
elif [ "$CTX_SIZE" -gt 0 ]; then
92134
CTX_PCT=${CTX_USED_PCT%%.*}
93135
CTX_NUM_STR="$(format_tokens $CTX_USED_TOKENS)/$(format_tokens $CTX_TOTAL_TOKENS)"
94136
else
@@ -97,7 +139,6 @@ else
97139
fi
98140

99141
# --- Git branch & diff ---
100-
CWD=$(echo "$input" | jq -r '.workspace.current_dir // "."')
101142
GIT_BRANCH=""
102143
if git -C "$CWD" rev-parse --git-dir > /dev/null 2>&1; then
103144
BRANCH=$(git -C "$CWD" --no-optional-locks branch --show-current 2>/dev/null)

tests/statusline.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
5+
6+
bash -n "$repo_root/.claude/statusline.sh"
7+
8+
if ! command -v jq >/dev/null 2>&1; then
9+
echo "skip: jq not found"
10+
exit 0
11+
fi
12+
13+
tmp_dir=$(mktemp -d)
14+
trap 'rm -rf "$tmp_dir"' EXIT INT TERM
15+
16+
stdout="$tmp_dir/stdout"
17+
stderr="$tmp_dir/stderr"
18+
marker="STATUSLINE_EVAL_PROOF"
19+
20+
payload='{"model":{"display_name":"demo"},"context_window":{"context_window_size":"$(printf STATUSLINE_EVAL_PROOF >&2)","used_percentage":"$(printf STATUSLINE_EVAL_PROOF >&2)","current_usage":{"input_tokens":"$(printf STATUSLINE_EVAL_PROOF >&2)","cache_creation_input_tokens":0,"cache_read_input_tokens":0},"total_input_tokens":0,"total_output_tokens":0},"rate_limits":{"five_hour":{"used_percentage":"$(printf STATUSLINE_EVAL_PROOF >&2)","resets_at":0},"seven_day":{"used_percentage":0,"resets_at":0}},"workspace":{"current_dir":"."}}'
21+
22+
if ! printf '%s\n' "$payload" | "$repo_root/.claude/statusline.sh" >"$stdout" 2>"$stderr"; then
23+
cat "$stdout" >&2
24+
cat "$stderr" >&2
25+
echo "statusline failed on malformed numeric fields" >&2
26+
exit 1
27+
fi
28+
29+
if grep -q "$marker" "$stderr"; then
30+
cat "$stderr" >&2
31+
echo "statusline executed JSON-derived shell syntax" >&2
32+
exit 1
33+
fi

0 commit comments

Comments
 (0)