Skip to content

Commit 338c362

Browse files
committed
fix(step-7,step-8,step-final): tighten MCP match, atomic SHA verify, guard python3, clean sed .bak
- step-7: tighten all 'claude mcp list | grep' checks from loose substring ('github') to anchored prefix ('^github:') so user MCPs named 'github-mirror' or 'my-github-fork' don't produce false positives in the non-interactive autodetect, install_github idempotency guard, install_github verification, and check_registered self-test helper. - step-7: always install /gitfix even when user skips the GitHub MCP prompt (empty selection at choose_tools) — /gitfix requires no credentials and is a canonical Step 7 deliverable. Non-interactive curl-pipe path also now installs /gitfix + runs self-test + summary before exit. - step-7: guard the apt keyring install against a curl failure poisoning /usr/share/keyrings/ with an empty file — download to mktemp, verify non-empty, then 'sudo install -m 0644' into place. - step-7: hard-check python3 presence before the token-injection heredoc. Without python3 the MCP server registers but with no GITHUB_PERSONAL_ACCESS_TOKEN — surface that as a soft_fail with a manual-fix pointer instead of silently shipping a broken MCP entry. Also change '[saved]' -> '[captured]' after the token read since the token is only in memory at that point. - step-8: stage the skill download to a mktemp temp file and verify SHA-256 BEFORE moving it to $SKILL_FILE. Previously a tampered/corrupted file was written directly to the skill path and only flagged post-hoc — Claude would have loaded the bad skill until re-install. Now: download -> verify non-empty -> verify sha256 against pinned digest -> verify keyword -> atomic mv. Tampered content never lands on disk. - step-final: clean up the '.bak' file that sed -i.bak leaves behind when removing the stale 'alias ctg=' line from $SHELL_RC, so we don't litter ~/.zshrc.bak in every user's home directory. All four step scripts pass bash -n and shellcheck (default severity).
1 parent 79d24f1 commit 338c362

3 files changed

Lines changed: 93 additions & 35 deletions

File tree

step-7/step-7-install.sh

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,22 @@ install_gh() {
102102
brew install gh || { soft_fail "GitHub CLI installation failed"; return; }
103103
else
104104
if command -v apt-get &>/dev/null; then
105-
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null
105+
# Download keyring to a temp file first so a curl failure can't
106+
# poison /usr/share/keyrings with an empty/truncated file.
107+
local keyring_tmp
108+
keyring_tmp="$(mktemp)" || { soft_fail "Could not create temp file"; return; }
109+
if ! curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o "$keyring_tmp"; then
110+
rm -f "$keyring_tmp"
111+
soft_fail "Failed to download GitHub CLI keyring"
112+
return
113+
fi
114+
if ! [ -s "$keyring_tmp" ]; then
115+
rm -f "$keyring_tmp"
116+
soft_fail "Downloaded GitHub CLI keyring is empty"
117+
return
118+
fi
119+
sudo install -m 0644 "$keyring_tmp" /usr/share/keyrings/githubcli-archive-keyring.gpg
120+
rm -f "$keyring_tmp"
106121
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
107122
if sudo apt-get update -qq && sudo apt-get install -y -qq gh; then
108123
:
@@ -133,8 +148,10 @@ choose_tools() {
133148
info "Non-interactive mode detected (running via curl pipe)"
134149
CHOICES=""
135150

136-
# Auto-detect already-installed tools
137-
if claude mcp list 2>/dev/null | grep -q "github" 2>/dev/null; then
151+
# Auto-detect already-installed tools.
152+
# Anchor to start-of-line + literal name + ":" so we don't match
153+
# user-defined MCP servers like "my-github-fork" or "github-mirror".
154+
if claude mcp list 2>/dev/null | grep -qE '^github:' 2>/dev/null; then
138155
CHOICES="$CHOICES 1"
139156
INSTALLED_GITHUB=true
140157
fi
@@ -143,12 +160,16 @@ choose_tools() {
143160
info "Found already-installed tools — verifying configuration"
144161
return
145162
else
163+
# No GitHub MCP yet and we can't prompt — still install /gitfix (no creds needed).
146164
echo ""
147-
echo -e "${YELLOW} Step 7 requires interactive input for API credentials.${NC}"
148-
echo -e "${YELLOW} Run it directly in your terminal:${NC}"
165+
echo -e "${YELLOW} Step 7 requires interactive input to set up the GitHub MCP.${NC}"
166+
echo -e "${YELLOW} Run it directly in your terminal to finish:${NC}"
149167
echo ""
150168
echo " bash <(curl -fsSL https://raw.githubusercontent.com/lorecraft-io/cli-maxxing/main/step-7/step-7-install.sh)"
151169
echo ""
170+
info "Continuing with non-interactive /gitfix install..."
171+
install_gitfix
172+
run_self_test
152173
print_summary
153174
exit 0
154175
fi
@@ -167,9 +188,10 @@ choose_tools() {
167188
echo ""
168189

169190
if [ -z "$CHOICES" ]; then
170-
warn "No tools selected. Nothing to install."
171-
print_summary
172-
exit 0
191+
# GitHub MCP is optional, but /gitfix is always installed in Step 7.
192+
# Return instead of exiting so main() can still install /gitfix.
193+
warn "No GitHub MCP selected — continuing to install /gitfix."
194+
return
173195
fi
174196
}
175197

@@ -179,7 +201,7 @@ choose_tools() {
179201
install_github() {
180202
info "Installing GitHub MCP server..."
181203

182-
if claude mcp list 2>/dev/null | grep -q "github"; then
204+
if claude mcp list 2>/dev/null | grep -qE '^github:'; then
183205
success "GitHub MCP already installed"
184206
INSTALLED_GITHUB=true
185207
return
@@ -202,7 +224,7 @@ install_github() {
202224
echo ""
203225

204226
read -rsp " GitHub Personal Access Token (ghp_...): " GITHUB_TOKEN
205-
echo " [saved]"
227+
echo " [captured]"
206228
echo ""
207229

208230
if [ -z "$GITHUB_TOKEN" ]; then
@@ -224,6 +246,11 @@ install_github() {
224246
# Inject the token directly into the config entry (claude mcp add --scope user
225247
# does not support -e flags in all CLI versions, so we patch the env block).
226248
# Token is passed via env var (not argv) to avoid leaking it in `ps` output.
249+
if ! command -v python3 &>/dev/null; then
250+
soft_fail "python3 not found — cannot inject token into MCP config. Install python3 and re-run Step 7, or add GITHUB_PERSONAL_ACCESS_TOKEN to ~/.claude.json manually."
251+
unset GITHUB_TOKEN GITHUB_TOKEN_VALUE
252+
return
253+
fi
227254
GITHUB_TOKEN_VALUE="$GITHUB_TOKEN" python3 - <<'PYEOF'
228255
import json, os
229256
@@ -245,7 +272,7 @@ else:
245272
print("WARNING: github entry not found in MCP config — token not injected.")
246273
PYEOF
247274

248-
if claude mcp list 2>/dev/null | grep -q "github"; then
275+
if claude mcp list 2>/dev/null | grep -qE '^github:'; then
249276
success "GitHub MCP installed"
250277
INSTALLED_GITHUB=true
251278
else
@@ -307,8 +334,10 @@ run_self_test() {
307334

308335
check_registered() {
309336
local label="$1"
310-
local needle="$2"
311-
if claude mcp list 2>/dev/null | grep -q "$needle"; then
337+
local name="$2"
338+
# Anchor to start-of-line + literal name + ":" so we don't match
339+
# user-defined MCP servers whose names contain the target as a substring.
340+
if claude mcp list 2>/dev/null | grep -qE "^${name}:"; then
312341
success "TEST: $label MCP registered"
313342
TEST_PASS=$((TEST_PASS + 1))
314343
else

step-8/step-8-install.sh

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -158,54 +158,79 @@ install_skill() {
158158
fi
159159

160160
info "Downloading /safetycheck skill..."
161-
if curl -fsSL "$SKILL_URL" -o "$SKILL_FILE" 2>/dev/null; then
162-
success "Skill downloaded to $SKILL_FILE"
161+
162+
# Stage the download in a temp file so we can verify SHA-256 BEFORE the
163+
# file lands at the skill path. Without this, a bad file could be written
164+
# to disk and then only flagged on the next line — the tampered skill
165+
# would still be visible to Claude until the user re-runs Step 8.
166+
SKILL_TMP="$(mktemp "${TMPDIR:-/tmp}/safetycheck-skill.XXXXXX")" || {
167+
soft_fail "Could not create temp file for skill download"
168+
return
169+
}
170+
171+
SOURCE=""
172+
if curl -fsSL "$SKILL_URL" -o "$SKILL_TMP" 2>/dev/null && [ -s "$SKILL_TMP" ]; then
173+
SOURCE="download"
163174
else
164175
warn "Download failed — attempting fallback install..."
165176
# Fallback: if the download fails (e.g., repo not yet public),
166177
# check if the script was run from the CLI-MAXXING repo itself
167178
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
168179
LOCAL_SKILL="$SCRIPT_DIR/safetycheck-skill/SKILL.md"
169180

170-
if [ -f "$LOCAL_SKILL" ]; then
171-
cp "$LOCAL_SKILL" "$SKILL_FILE"
172-
success "Skill installed from local copy"
181+
if [ -f "$LOCAL_SKILL" ] && [ -s "$LOCAL_SKILL" ]; then
182+
cp "$LOCAL_SKILL" "$SKILL_TMP"
183+
SOURCE="local"
173184
else
185+
rm -f "$SKILL_TMP"
174186
soft_fail "Could not download or find the skill file"
175187
return
176188
fi
177189
fi
178190

179-
# Verify the file is non-empty
180-
if [ ! -s "$SKILL_FILE" ]; then
181-
soft_fail "Skill file downloaded but is empty — try again later"
182-
rm -f "$SKILL_FILE"
191+
# Verify the staged file is non-empty before SHA check
192+
if [ ! -s "$SKILL_TMP" ]; then
193+
rm -f "$SKILL_TMP"
194+
soft_fail "Skill file is empty — try again later"
183195
return
184196
fi
185197

186-
# Verify SHA-256 integrity — protects against corrupted download or tampered content
198+
# Verify SHA-256 integrity BEFORE moving into place — protects against
199+
# corrupted download, MITM, or tampered upstream content.
200+
ACTUAL_SHA=""
187201
if command -v shasum &>/dev/null; then
188-
ACTUAL_SHA=$(shasum -a 256 "$SKILL_FILE" | cut -d' ' -f1)
189-
if [ "$ACTUAL_SHA" = "$SKILL_SHA256" ]; then
190-
success "Skill file integrity verified (sha256 match)"
191-
else
192-
soft_fail "Skill file sha256 mismatch — file may be corrupt or tampered. Expected: ${SKILL_SHA256:0:16}..."
193-
fi
202+
ACTUAL_SHA=$(shasum -a 256 "$SKILL_TMP" | cut -d' ' -f1)
194203
elif command -v sha256sum &>/dev/null; then
195-
ACTUAL_SHA=$(sha256sum "$SKILL_FILE" | cut -d' ' -f1)
204+
ACTUAL_SHA=$(sha256sum "$SKILL_TMP" | cut -d' ' -f1)
205+
fi
206+
207+
if [ -n "$ACTUAL_SHA" ]; then
196208
if [ "$ACTUAL_SHA" = "$SKILL_SHA256" ]; then
197209
success "Skill file integrity verified (sha256 match)"
198210
else
199-
soft_fail "Skill file sha256 mismatch — file may be corrupt or tampered. Expected: ${SKILL_SHA256:0:16}..."
211+
rm -f "$SKILL_TMP"
212+
soft_fail "Skill file sha256 mismatch — refusing to install. Expected: ${SKILL_SHA256:0:16}..., got: ${ACTUAL_SHA:0:16}..."
213+
return
200214
fi
215+
else
216+
warn "Neither shasum nor sha256sum found — installing without integrity check"
201217
fi
202218

203-
# Verify the file contains expected content
204-
if grep -q "safetycheck" "$SKILL_FILE" 2>/dev/null; then
205-
success "Skill file content verified"
219+
# Verify expected content keyword
220+
if ! grep -q "safetycheck" "$SKILL_TMP" 2>/dev/null; then
221+
rm -f "$SKILL_TMP"
222+
soft_fail "Skill file does not contain expected content — refusing to install"
223+
return
224+
fi
225+
226+
# Atomic move into place — skill at $SKILL_FILE only after every check passes.
227+
mv "$SKILL_TMP" "$SKILL_FILE"
228+
if [ "$SOURCE" = "download" ]; then
229+
success "Skill downloaded to $SKILL_FILE"
206230
else
207-
soft_fail "Skill file does not contain expected content — may be corrupt"
231+
success "Skill installed from local copy"
208232
fi
233+
success "Skill file content verified"
209234

210235
echo ""
211236
}

step-final/step-final-install.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,11 @@ fi
348348
# --- ctg script (token-guarded — not an alias) ---
349349
# Migrate: remove stale alias if step-1 hasn't cleaned it up yet
350350
if grep -q 'alias ctg=' "$SHELL_RC" 2>/dev/null; then
351+
# sed -i.bak works on both BSD (macOS) and GNU sed. The .bak file is
352+
# only needed as a safety net during the edit — clean it up afterward
353+
# so we don't leave ~/.zshrc.bak lying around.
351354
sed -i.bak '/alias ctg=/d' "$SHELL_RC"
355+
rm -f "${SHELL_RC}.bak"
352356
warn "HEALTH: removed stale ctg alias from $SHELL_RC — replaced by ~/.local/bin/ctg"
353357
fi
354358
if [ -x "$HOME/.local/bin/ctg" ]; then

0 commit comments

Comments
 (0)