Skip to content

Commit 860a5de

Browse files
committed
fix(runner): avoid ARG_MAX on large PR diffs
Write the full prompt to /tmp/prompt.md and use awk-based placeholder substitution instead of bash variable expansion. Large fields (diff, file list, description) are inserted from temp files to avoid hitting the kernel's ARG_MAX limit (~2MB). The claude CLI receives a short bootstrap prompt pointing to the file. Root cause: bash expands the entire diff into a shell variable, then passes it as a command argument — PRs with >2MB of changes cause "Argument list too long".
1 parent cf66c50 commit 860a5de

1 file changed

Lines changed: 43 additions & 9 deletions

File tree

infra/docker/claude-runner/entrypoint.sh

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,46 @@ PR_DESCRIPTION=$(jq -r '.body // "No description provided."' /tmp/pr_meta.json)
7171
PR_DIFF=$(cat /tmp/pr_diff.txt)
7272
FILE_LIST=$(jq -r '.files[].path' /tmp/pr_meta.json)
7373

74-
# Read the prompt template and substitute placeholders
75-
PROMPT=$(cat "/skills/$SKILL_NAME/prompt.md")
76-
PROMPT="${PROMPT//\{\{PR_NUMBER\}\}/$PR_NUMBER}"
77-
PROMPT="${PROMPT//\{\{PR_TITLE\}\}/$PR_TITLE}"
78-
PROMPT="${PROMPT//\{\{PR_AUTHOR\}\}/$PR_AUTHOR}"
79-
PROMPT="${PROMPT//\{\{PR_DESCRIPTION\}\}/$PR_DESCRIPTION}"
80-
PROMPT="${PROMPT//\{\{FILE_LIST\}\}/$FILE_LIST}"
81-
PROMPT="${PROMPT//\{\{PR_DIFF\}\}/$PR_DIFF}"
74+
# Build the prompt file from template + PR context.
75+
# Write to a file instead of holding in a shell variable to avoid
76+
# "Argument list too long" on large PRs (ARG_MAX ~2MB).
77+
PROMPT_FILE=/tmp/prompt.md
78+
{
79+
cat "/skills/$SKILL_NAME/prompt.md"
80+
} > "$PROMPT_FILE"
81+
82+
# Substitute placeholders using sed (handles large diffs without hitting ARG_MAX).
83+
# Small fields first (safe as shell vars), then large fields via temp files.
84+
sed -i "s|{{PR_NUMBER}}|$PR_NUMBER|g" "$PROMPT_FILE"
85+
sed -i "s|{{PR_TITLE}}|$PR_TITLE|g" "$PROMPT_FILE"
86+
sed -i "s|{{PR_AUTHOR}}|$PR_AUTHOR|g" "$PROMPT_FILE"
87+
88+
# Large fields: use sed with file-read to avoid shell expansion limits.
89+
# PR_DESCRIPTION, FILE_LIST, and PR_DIFF are written to temp files
90+
# and inserted via sed's r command with a marker-delete approach.
91+
echo "$PR_DESCRIPTION" > /tmp/pr_description.txt
92+
echo "$FILE_LIST" > /tmp/pr_filelist.txt
93+
94+
# For each large placeholder: replace the line containing it with the file contents.
95+
# Using awk because sed r-command can't replace inline — it only appends.
96+
for placeholder_pair in \
97+
"{{PR_DESCRIPTION}}:/tmp/pr_description.txt" \
98+
"{{FILE_LIST}}:/tmp/pr_filelist.txt" \
99+
"{{PR_DIFF}}:/tmp/pr_diff.txt"; do
100+
marker="${placeholder_pair%%:*}"
101+
file="${placeholder_pair##*:}"
102+
if grep -qF "$marker" "$PROMPT_FILE" && [ -f "$file" ]; then
103+
awk -v marker="$marker" -v file="$file" '
104+
index($0, marker) {
105+
while ((getline line < file) > 0) print line
106+
close(file)
107+
next
108+
}
109+
{ print }
110+
' "$PROMPT_FILE" > /tmp/prompt_tmp.md
111+
mv /tmp/prompt_tmp.md "$PROMPT_FILE"
112+
fi
113+
done
82114

83115
# ---------------------------------------------------------------------------
84116
# Multi-turn conversation via per-turn invocations
@@ -102,7 +134,9 @@ mkfifo "$FIFO"
102134
exec 3<>"$FIFO"
103135

104136
# First turn: initial skill prompt (one-shot, creates the conversation)
105-
claude -p "$PROMPT" \
137+
# The full prompt is written to a file to avoid ARG_MAX on large PRs.
138+
# We pass a short bootstrap prompt that tells Claude to read the file.
139+
claude -p "Read the file /tmp/prompt.md and follow its instructions exactly. It contains a skill prompt with PR context. Start immediately." \
106140
--output-format stream-json \
107141
--verbose \
108142
--dangerously-skip-permissions \

0 commit comments

Comments
 (0)