Skip to content

Commit 9c73e68

Browse files
404prefrontalcortexnotfoundBo Bobsonclaude
authored
fix(bash): sed replacement escaping, BSD portability, dead cleanup in update-agent-context.sh (#2090)
* fix(bash): sed replacement escaping, BSD portability, dead cleanup code Three bugs in update-agent-context.sh: 1. **sed escaping targets wrong side** (line 318-320): The escaping function escapes regex pattern characters (`[`, `.`, `*`, `^`, `$`, `+`, `{`, `}`, `|`) but these variables are used as sed *replacement* strings, not patterns. Only `&` (insert matched text), `\` (escape char), and `|` (our sed delimiter) are special in the replacement context. Also adds escaping for `project_name` which was used unescaped. 2. **BSD sed newline insertion fails on macOS** (line 364-366): Uses bash variable expansion to insert a literal newline into a sed replacement string. This works on GNU sed (Linux) but fails silently on BSD sed (macOS). Replaced with portable awk approach that works on both platforms. 3. **cleanup() removes non-existent files** (line 125-126): The cleanup trap attempts `rm -f /tmp/agent_update_*_$$` and `rm -f /tmp/manual_additions_$$` but the script never creates files matching these patterns — all temp files use `mktemp`. The wildcard with `$$` (PID) in /tmp could theoretically match unrelated files. Fixes #154 (macOS sed failure) Fixes #293 (sed expression errors) Related: #338 (shellcheck findings) * fix: restore forge case and revert copilot path change Address PR review feedback: - Restore forge) case in update_specific_agent since src/specify_cli/integrations/forge/__init__.py still exists - Revert COPILOT_FILE path from .github/agents/ back to .github/ to stay consistent with Python integration and tests - Restore FORGE_FILE variable, comments, and usage strings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: extract repeated sed escaping into _esc_sed helper Address Gemini review feedback — the inline sed escaping pattern appeared 7 times in create_new_agent_file(). Extract to a single helper function for maintainability and readability. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore combined AGENTS_FILE label in update_all_existing_agents Gemini correctly identified that splitting AGENTS_FILE updates into individual calls is redundant — _update_if_new deduplicates by realpath, so only the first call logs. Restore the combined label and add back missing Pi reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove pre-escaped && in JS/TS commands now that _esc_sed handles it The old code manually pre-escaped & as \& in get_commands_for_language because the broken escaping function didn't handle &. Now that _esc_sed properly escapes replacement-side specials, the pre-escaping causes double-escaping: && becomes \&\& in generated files. Found by blind audit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: split awk && mv to let set -e catch awk failures Under set -e, the left side of && does not trigger errexit on failure. Split into two statements so awk failures are fatal instead of silent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: guard empty _CLEANUP_FILES array for Bash 3.2 compatibility On Bash 3.2, the ${arr[@]+"${arr[@]}"} pattern expands to a single empty string when the array is empty, causing rm to target .bak and .tmp in the current directory. Use explicit length check instead, which also avoids the word-splitting risk of unquoted expansion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Bo Bobson <bo@noneofyourbusiness.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8472e44 commit 9c73e68

File tree

1 file changed

+32
-16
lines changed

1 file changed

+32
-16
lines changed

scripts/bash/update-agent-context.sh

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,19 @@ log_warning() {
117117
echo "WARNING: $1" >&2
118118
}
119119

120+
# Track temporary files for cleanup on interrupt
121+
_CLEANUP_FILES=()
122+
120123
# Cleanup function for temporary files
121124
cleanup() {
122125
local exit_code=$?
123126
# Disarm traps to prevent re-entrant loop
124127
trap - EXIT INT TERM
125-
rm -f /tmp/agent_update_*_$$
126-
rm -f /tmp/manual_additions_$$
128+
if [ ${#_CLEANUP_FILES[@]} -gt 0 ]; then
129+
for f in "${_CLEANUP_FILES[@]}"; do
130+
rm -f "$f" "$f.bak" "$f.tmp"
131+
done
132+
fi
127133
exit $exit_code
128134
}
129135

@@ -268,7 +274,7 @@ get_commands_for_language() {
268274
echo "cargo test && cargo clippy"
269275
;;
270276
*"JavaScript"*|*"TypeScript"*)
271-
echo "npm test \\&\\& npm run lint"
277+
echo "npm test && npm run lint"
272278
;;
273279
*)
274280
echo "# Add commands for $lang"
@@ -281,10 +287,15 @@ get_language_conventions() {
281287
echo "$lang: Follow standard conventions"
282288
}
283289

290+
# Escape sed replacement-side specials for | delimiter.
291+
# & and \ are replacement-side specials; | is our sed delimiter.
292+
_esc_sed() { printf '%s\n' "$1" | sed 's/[\\&|]/\\&/g'; }
293+
284294
create_new_agent_file() {
285295
local target_file="$1"
286296
local temp_file="$2"
287-
local project_name="$3"
297+
local project_name
298+
project_name=$(_esc_sed "$3")
288299
local current_date="$4"
289300

290301
if [[ ! -f "$TEMPLATE_FILE" ]]; then
@@ -307,18 +318,19 @@ create_new_agent_file() {
307318
# Replace template placeholders
308319
local project_structure
309320
project_structure=$(get_project_structure "$NEW_PROJECT_TYPE")
321+
project_structure=$(_esc_sed "$project_structure")
310322

311323
local commands
312324
commands=$(get_commands_for_language "$NEW_LANG")
313-
325+
314326
local language_conventions
315327
language_conventions=$(get_language_conventions "$NEW_LANG")
316-
317-
# Perform substitutions with error checking using safer approach
318-
# Escape special characters for sed by using a different delimiter or escaping
319-
local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g')
320-
local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g')
321-
local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g')
328+
329+
local escaped_lang=$(_esc_sed "$NEW_LANG")
330+
local escaped_framework=$(_esc_sed "$NEW_FRAMEWORK")
331+
commands=$(_esc_sed "$commands")
332+
language_conventions=$(_esc_sed "$language_conventions")
333+
local escaped_branch=$(_esc_sed "$CURRENT_BRANCH")
322334

323335
# Build technology stack and recent change strings conditionally
324336
local tech_stack
@@ -361,17 +373,18 @@ create_new_agent_file() {
361373
fi
362374
done
363375

364-
# Convert \n sequences to actual newlines
365-
newline=$(printf '\n')
366-
sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"
376+
# Convert literal \n sequences to actual newlines (portable — works on BSD + GNU)
377+
awk '{gsub(/\\n/,"\n")}1' "$temp_file" > "$temp_file.tmp"
378+
mv "$temp_file.tmp" "$temp_file"
367379

368-
# Clean up backup files
369-
rm -f "$temp_file.bak" "$temp_file.bak2"
380+
# Clean up backup files from sed -i.bak
381+
rm -f "$temp_file.bak"
370382

371383
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
372384
if [[ "$target_file" == *.mdc ]]; then
373385
local frontmatter_file
374386
frontmatter_file=$(mktemp) || return 1
387+
_CLEANUP_FILES+=("$frontmatter_file")
375388
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
376389
cat "$temp_file" >> "$frontmatter_file"
377390
mv "$frontmatter_file" "$temp_file"
@@ -395,6 +408,7 @@ update_existing_agent_file() {
395408
log_error "Failed to create temporary file"
396409
return 1
397410
}
411+
_CLEANUP_FILES+=("$temp_file")
398412

399413
# Process the file in one pass
400414
local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK")
@@ -519,6 +533,7 @@ update_existing_agent_file() {
519533
if ! head -1 "$temp_file" | grep -q '^---'; then
520534
local frontmatter_file
521535
frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; }
536+
_CLEANUP_FILES+=("$frontmatter_file")
522537
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
523538
cat "$temp_file" >> "$frontmatter_file"
524539
mv "$frontmatter_file" "$temp_file"
@@ -571,6 +586,7 @@ update_agent_file() {
571586
log_error "Failed to create temporary file"
572587
return 1
573588
}
589+
_CLEANUP_FILES+=("$temp_file")
574590

575591
if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then
576592
if mv "$temp_file" "$target_file"; then

0 commit comments

Comments
 (0)