Refactor to add skill/ directory with bootstrap scripts#3
Conversation
- Move skill.md to skill/skill.md - Add bootstrap.sh for lazy-loading binaries from GitHub releases - Add wrapper scripts for each tool (create-wallet, get-address, pay, x402curl, x402-config) - Add skill/scripts/ directory for downloaded binaries (gitignored) - Update install.md with new installation instructions The bootstrap approach: 1. Wrapper scripts detect platform (OS + arch) 2. On first run, download appropriate zip from GitHub releases 3. Extract binaries to skill/scripts/ 4. Execute the real binary with original arguments Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors the x402 skill to use a lazy-loading approach for binary distribution. Instead of requiring users to manually download platform-specific binaries, wrapper scripts now automatically download and cache them from GitHub releases on first use.
Changes:
- Moved
skill.mdinto askill/directory with bootstrap infrastructure - Added
bootstrap.shcontaining platform detection, download, and extraction logic - Created wrapper scripts for all five tools that source bootstrap.sh and delegate to actual binaries
- Updated installation documentation to reflect the new bootstrap approach
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| skill/bootstrap.sh | Core bootstrap logic for platform detection, downloading binaries from GitHub releases, and executing tools |
| skill/create-wallet | Wrapper script that bootstraps and delegates to the create-wallet binary |
| skill/get-address | Wrapper script that bootstraps and delegates to the get-address binary |
| skill/pay | Wrapper script that bootstraps and delegates to the pay binary |
| skill/x402curl | Wrapper script that bootstraps and delegates to the x402curl binary |
| skill/x402-config | Wrapper script that bootstraps and delegates to the x402-config binary |
| skill/scripts/.gitignore | Ensures downloaded binaries are not committed to the repository |
| skill/scripts/.gitkeep | Placeholder to maintain the scripts directory in git |
| skill/skill.md | Updated with installation instructions explaining the bootstrap process |
| install.md | Completely rewritten with new installation instructions for the bootstrap approach |
Comments suppressed due to low confidence (2)
skill/skill.md:14
- Inconsistent path reference. The documentation states binaries are extracted to
skill/scripts/, but based on the actual installation location (~/.claude/skills/x402/) and the bootstrap.sh script (which usesSCRIPTS_DIR="${SCRIPT_DIR}/scripts"), the correct path relative to the script location would be justscripts/, notskill/scripts/.
The absolute path shown at line 24 also incorrectly includes an extra skill/ subdirectory.
skill/skill.md:24
- Inconsistent path reference. The correct path after installation should be
~/.claude/skills/x402/scripts/(without the extraskill/subdirectory). This is consistent with how the bootstrap.sh script resolves paths and the directory structure shown in install.md.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
skill/bootstrap.sh
Outdated
| fi | ||
|
|
||
| # Make binaries executable (not needed on Windows) | ||
| if [ "$(uname -s)" != "MINGW"* ] && [ "$(uname -s)" != "MSYS"* ] && [ "$(uname -s)" != "CYGWIN"* ]; then |
There was a problem hiding this comment.
The string comparison syntax is incorrect. The [ ] test command doesn't support glob pattern matching with !=. The current condition will always evaluate to false because it's doing a literal string comparison.
Use a case statement or [[ ]] instead. For example:
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*) ;; # Skip chmod on Windows
*) chmod +x "${SCRIPTS_DIR}"/* ;;
esacOr use double brackets:
if [[ "$(uname -s)" != MINGW* ]] && [[ "$(uname -s)" != MSYS* ]] && [[ "$(uname -s)" != CYGWIN* ]]; then| if [ "$(uname -s)" != "MINGW"* ] && [ "$(uname -s)" != "MSYS"* ] && [ "$(uname -s)" != "CYGWIN"* ]; then | |
| if [[ "$(uname -s)" != MINGW* ]] && [[ "$(uname -s)" != MSYS* ]] && [[ "$(uname -s)" != CYGWIN* ]]; then |
skill/bootstrap.sh
Outdated
|
|
||
| # Make binaries executable (not needed on Windows) | ||
| if [ "$(uname -s)" != "MINGW"* ] && [ "$(uname -s)" != "MSYS"* ] && [ "$(uname -s)" != "CYGWIN"* ]; then | ||
| chmod +x "${SCRIPTS_DIR}"/* |
There was a problem hiding this comment.
The wildcard glob "${SCRIPTS_DIR}"/* will match all files including .gitignore and .gitkeep, making them executable unnecessarily. Consider being more selective by only making actual binary files executable, or excluding dotfiles from the chmod operation.
For example:
find "${SCRIPTS_DIR}" -maxdepth 1 -type f ! -name '.*' -exec chmod +x {} +Or if the binaries have specific names:
for tool in create-wallet get-address pay x402curl x402-config; do
[ -f "${SCRIPTS_DIR}/${tool}" ] && chmod +x "${SCRIPTS_DIR}/${tool}"
done| chmod +x "${SCRIPTS_DIR}"/* | |
| find "${SCRIPTS_DIR}" -maxdepth 1 -type f ! -name '.*' -exec chmod +x {} + |
| echo "Installed tools:" | ||
| ls "${SCRIPTS_DIR}" | ||
| ``` | ||
| 1. Wrapper scripts in `skill/` detect your platform |
There was a problem hiding this comment.
Inconsistent path reference. The documentation references skill/scripts/ subdirectory, but based on the installation command at line 15 (cp -r /tmp/x402-repo/skill "$SKILL_DIR"), the actual path after installation would be ~/.claude/skills/x402/scripts/ (without the skill/ subdirectory).
This inconsistency is also present at lines 31 and 74-75. The path should be just scripts/ to match the actual directory structure shown at lines 39-50.
| 3. Extract to `~/.claude/skills/x402/skill/scripts/` | ||
| 4. Make executable: `chmod +x ~/.claude/skills/x402/skill/scripts/*` |
There was a problem hiding this comment.
Inconsistent path reference. Based on the installation command at line 15, the actual path after installation should be ~/.claude/skills/x402/scripts/ (without the skill/ subdirectory in between). This should be changed to match the correct directory structure.
| 3. Extract to `~/.claude/skills/x402/skill/scripts/` | |
| 4. Make executable: `chmod +x ~/.claude/skills/x402/skill/scripts/*` | |
| 3. Extract to `~/.claude/skills/x402/scripts/` | |
| 4. Make executable: `chmod +x ~/.claude/skills/x402/scripts/*` |
| ``` | ||
| 1. Wrapper scripts in `skill/` detect your platform | ||
| 2. On first run, binaries are downloaded from GitHub releases | ||
| 3. Binaries are cached in `skill/scripts/` |
There was a problem hiding this comment.
Inconsistent path reference. Based on the installation command at line 15, the correct path should be just scripts/ (not skill/scripts/). After installation, the binaries are cached directly in ~/.claude/skills/x402/scripts/ as shown in the directory structure at lines 39-50.
skill/bootstrap.sh
Outdated
| # Download | ||
| if command -v curl &>/dev/null; then | ||
| curl -sL -o "$zip_file" "$url" | ||
| else | ||
| wget -q -O "$zip_file" "$url" | ||
| fi | ||
|
|
||
| # Extract | ||
| if command -v unzip &>/dev/null; then | ||
| unzip -q -o "$zip_file" -d "${SCRIPTS_DIR}" | ||
| else | ||
| echo "Error: unzip not found. Please install unzip." >&2 | ||
| rm -rf "$temp_dir" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Make binaries executable (not needed on Windows) | ||
| if [ "$(uname -s)" != "MINGW"* ] && [ "$(uname -s)" != "MSYS"* ] && [ "$(uname -s)" != "CYGWIN"* ]; then | ||
| chmod +x "${SCRIPTS_DIR}"/* | ||
| fi | ||
|
|
||
| # Cleanup | ||
| rm -rf "$temp_dir" | ||
|
|
||
| echo "x402 tools installed successfully." >&2 | ||
| } | ||
|
|
||
| # Main bootstrap function | ||
| # Usage: bootstrap_and_run <tool_name> [args...] | ||
| bootstrap_and_run() { | ||
| local tool_name="$1" | ||
| shift | ||
|
|
||
| local platform | ||
| local binary_path | ||
| local binary_name | ||
|
|
||
| platform=$(detect_platform) | ||
|
|
||
| # Determine binary name (add .exe for Windows) | ||
| if [[ "$platform" == windows-* ]]; then | ||
| binary_name="${tool_name}.exe" | ||
| else | ||
| binary_name="${tool_name}" | ||
| fi | ||
|
|
||
| binary_path="${SCRIPTS_DIR}/${binary_name}" | ||
|
|
||
| # Check if binary exists, download if not | ||
| if [ ! -x "$binary_path" ]; then | ||
| local download_url | ||
| download_url=$(get_download_url "$platform") | ||
| download_binaries "$platform" "$download_url" | ||
| fi | ||
|
|
||
| # Verify binary exists after download | ||
| if [ ! -x "$binary_path" ]; then | ||
| echo "Error: Binary ${binary_name} not found after download." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Execute the tool | ||
| exec "$binary_path" "$@" |
There was a problem hiding this comment.
The bootstrap logic downloads and executes prebuilt binaries from https://github.com/second-state/x402-agent-tools using the mutable releases/latest endpoint without any integrity verification or version pinning, which creates a supply-chain risk of arbitrary code execution if that repository, its releases, or the network path are compromised. An attacker who can tamper with the GitHub release artifacts (or the latest tag) could cause clients to transparently fetch and run a malicious binary whenever a wrapper script is executed. To mitigate this, pin downloads to immutable identifiers (e.g., specific release/version or checksummed artifacts) and verify the binary’s integrity (checksum/signature) before unzipping and exec-ing it.
- Remove individual wrapper scripts (create-wallet, get-address, pay, x402curl, x402-config) - Keep only bootstrap.sh for downloading binaries - Update skill.md with instructions for agent to: 1. Check if binaries exist in skill/scripts/ 2. Run bootstrap.sh if not installed 3. Run tools directly from skill/scripts/ - Update all command examples with full paths Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated 12 comments.
Comments suppressed due to low confidence (1)
skill/skill.md:27
- The PR description states "Add wrapper scripts for each tool (create-wallet, get-address, pay, x402curl, x402-config)" and describes that "Wrapper scripts in skill/ detect the platform" and "On first run, the appropriate zip is downloaded from GitHub releases". However, the actual implementation differs significantly:
- No wrapper scripts are included in this PR
- The documentation instructs users to manually run bootstrap.sh first, then call binaries directly from scripts/ directory
- There's no automatic lazy-loading on first tool invocation
This represents a discrepancy between the described approach (lazy-loading via wrapper scripts) and the implemented approach (manual bootstrap, then direct binary execution). Either the wrapper scripts need to be added, or the PR description needs to be updated to accurately describe the manual bootstrap approach.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| chmod +x "${SKILL_DIR}"/*.sh "${SKILL_DIR}/create-wallet" "${SKILL_DIR}/get-address" \ | ||
| "${SKILL_DIR}/pay" "${SKILL_DIR}/x402curl" "${SKILL_DIR}/x402-config" |
There was a problem hiding this comment.
The installation instructions reference wrapper scripts (create-wallet, get-address, pay, x402curl, x402-config) that should exist in the skill/ directory according to the PR description, but these files are not included in this pull request. The chmod command will fail because these wrapper scripts don't exist.
This appears to be a critical omission - either the wrapper scripts need to be added to this PR, or the installation instructions and documentation need to be updated to reflect a different approach (e.g., using bootstrap.sh directly or calling binaries from scripts/ directory).
| ├── create-wallet # Wrapper script | ||
| ├── get-address # Wrapper script | ||
| ├── pay # Wrapper script | ||
| ├── x402curl # Wrapper script | ||
| ├── x402-config # Wrapper script | ||
| └── scripts/ # Downloaded binaries (auto-populated) | ||
| ├── .gitignore | ||
| └── .gitkeep | ||
| ``` |
There was a problem hiding this comment.
The directory structure diagram shows wrapper scripts (create-wallet, get-address, pay, x402curl, x402-config) that are not included in this pull request. These files are referenced throughout the documentation but don't exist in the actual changes. This creates a discrepancy between documentation and implementation.
| ~/.claude/skills/x402/ | ||
| ├── skill.md # Skill definition for Claude | ||
| └── scripts/ | ||
| ├── create-wallet # Create new Ethereum wallet | ||
| ├── get-address # Get wallet public address | ||
| ├── pay # Transfer tokens | ||
| ├── x402curl # HTTP client with 402 handling | ||
| └── x402-config # Configuration management | ||
| ├── bootstrap.sh # Shared bootstrap logic | ||
| ├── create-wallet # Wrapper script | ||
| ├── get-address # Wrapper script | ||
| ├── pay # Wrapper script | ||
| ├── x402curl # Wrapper script | ||
| ├── x402-config # Wrapper script | ||
| └── scripts/ # Downloaded binaries (auto-populated) | ||
| ├── .gitignore | ||
| └── .gitkeep | ||
| ``` |
There was a problem hiding this comment.
The directory structure diagram is misleading because it shows ~/.claude/skills/x402/ as the root directory containing skill.md, bootstrap.sh, etc. However, based on the installation command at line 15 (cp -r /tmp/x402-repo/skill "$SKILL_DIR"), the actual structure would be:
~/.claude/skills/x402/skill/
├── skill.md # Skill definition for Claude
├── bootstrap.sh # Shared bootstrap logic
├── create-wallet # Wrapper script
├── get-address # Wrapper script
├── pay # Wrapper script
├── x402curl # Wrapper script
├── x402-config # Wrapper script
└── scripts/ # Downloaded binaries (auto-populated)
├── .gitignore
└── .gitkeep
Note the additional /skill/ subdirectory level that's missing from the current diagram. This is consistent with how skill.md references all paths (e.g., ~/.claude/skills/x402/skill/scripts/create-wallet).
| cp -r /tmp/x402-repo/skill "$SKILL_DIR" | ||
| rm -rf /tmp/x402-repo | ||
|
|
||
| ### Step 6: Verify Installation | ||
|
|
||
| ```bash | ||
| # List installed tools | ||
| ls -la "${SCRIPTS_DIR}" | ||
|
|
||
| # Verify executables work | ||
| "${SCRIPTS_DIR}/get-address" --help | ||
| # Make scripts executable | ||
| chmod +x "${SKILL_DIR}"/*.sh "${SKILL_DIR}/create-wallet" "${SKILL_DIR}/get-address" \ | ||
| "${SKILL_DIR}/pay" "${SKILL_DIR}/x402curl" "${SKILL_DIR}/x402-config" |
There was a problem hiding this comment.
There's a path inconsistency in the installation instructions. Line 15 copies the entire skill directory to SKILL_DIR with cp -r /tmp/x402-repo/skill "$SKILL_DIR", which creates the structure ~/.claude/skills/x402/skill/.... However, line 19 references wrapper scripts as if they're directly in SKILL_DIR: "${SKILL_DIR}/create-wallet" which would be ~/.claude/skills/x402/create-wallet.
For consistency, line 19 should either:
- Reference
"${SKILL_DIR}/skill/create-wallet"(to match the nested structure from line 15), or - Line 15 should copy the contents with
cp -r /tmp/x402-repo/skill/* "$SKILL_DIR"to flatten the structure
This affects lines 19-20, 88, and 105-107 which all use the wrong path pattern.
| download_url=$(curl -sL "$api_url" | grep -o "https://github.com/${REPO}/releases/download/[^\"]*${artifact_name}" | head -1) | ||
| elif command -v wget &>/dev/null; then | ||
| download_url=$(wget -qO- "$api_url" | grep -o "https://github.com/${REPO}/releases/download/[^\"]*${artifact_name}" | head -1) |
There was a problem hiding this comment.
The grep pattern used to extract the download URL from the GitHub API response may not be reliable across different platforms or if the API response format changes. The pattern grep -o "https://github.com/${REPO}/releases/download/[^\"]*${artifact_name}" relies on specific JSON formatting and assumes no quotes within the URL path.
Consider using a more robust JSON parser like jq if available, with a fallback to the current grep approach. For example:
if command -v jq &>/dev/null; then
download_url=$(curl -sL "$api_url" | jq -r ".assets[] | select(.name == \"${artifact_name}\") | .browser_download_url")
else
download_url=$(curl -sL "$api_url" | grep -o "https://github.com/${REPO}/releases/download/[^\"]*${artifact_name}" | head -1)
fiThis would make the script more robust, though the current approach may work adequately for the expected use case.
| download_url=$(curl -sL "$api_url" | grep -o "https://github.com/${REPO}/releases/download/[^\"]*${artifact_name}" | head -1) | |
| elif command -v wget &>/dev/null; then | |
| download_url=$(wget -qO- "$api_url" | grep -o "https://github.com/${REPO}/releases/download/[^\"]*${artifact_name}" | head -1) | |
| if command -v jq &>/dev/null; then | |
| download_url=$(curl -sL "$api_url" | jq -r ".assets[] | select(.name == \"${artifact_name}\") | .browser_download_url") | |
| else | |
| download_url=$(curl -sL "$api_url" | grep -o "https://github.com/${REPO}/releases/download/[^\"]*${artifact_name}" | head -1) | |
| fi | |
| elif command -v wget &>/dev/null; then | |
| if command -v jq &>/dev/null; then | |
| download_url=$(wget -qO- "$api_url" | jq -r ".assets[] | select(.name == \"${artifact_name}\") | .browser_download_url") | |
| else | |
| download_url=$(wget -qO- "$api_url" | grep -o "https://github.com/${REPO}/releases/download/[^\"]*${artifact_name}" | head -1) | |
| fi |
| curl -sL -o "$zip_file" "$url" | ||
| else | ||
| wget -q -O "$zip_file" "$url" | ||
| fi | ||
|
|
There was a problem hiding this comment.
The download commands (curl and wget) don't check for HTTP errors or download failures. If the download fails (e.g., 404, network error, server error), the script will still attempt to unzip an incomplete or non-existent file, leading to confusing error messages.
Consider adding error checking after the download:
if command -v curl &>/dev/null; then
if ! curl -sL -o "$zip_file" "$url"; then
echo "Error: Failed to download from ${url}" >&2
rm -rf "$temp_dir"
exit 1
fi
else
if ! wget -q -O "$zip_file" "$url"; then
echo "Error: Failed to download from ${url}" >&2
rm -rf "$temp_dir"
exit 1
fi
fi
# Verify the file was downloaded and is not empty
if [ ! -s "$zip_file" ]; then
echo "Error: Downloaded file is empty or missing" >&2
rm -rf "$temp_dir"
exit 1
fi| curl -sL -o "$zip_file" "$url" | |
| else | |
| wget -q -O "$zip_file" "$url" | |
| fi | |
| if ! curl -sL -o "$zip_file" "$url"; then | |
| echo "Error: Failed to download from ${url}" >&2 | |
| rm -rf "$temp_dir" | |
| exit 1 | |
| fi | |
| else | |
| if ! wget -q -O "$zip_file" "$url"; then | |
| echo "Error: Failed to download from ${url}" >&2 | |
| rm -rf "$temp_dir" | |
| exit 1 | |
| fi | |
| fi | |
| # Verify the file was downloaded and is not empty | |
| if [ ! -s "$zip_file" ]; then | |
| echo "Error: Downloaded file is empty or missing" >&2 | |
| rm -rf "$temp_dir" | |
| exit 1 | |
| fi |
| # Download | ||
| echo "Fetching from: ${url}" >&2 | ||
| if command -v curl &>/dev/null; then | ||
| curl -sL -o "$zip_file" "$url" | ||
| else | ||
| wget -q -O "$zip_file" "$url" | ||
| fi | ||
|
|
||
| # Extract | ||
| echo "Extracting binaries..." >&2 | ||
| if command -v unzip &>/dev/null; then | ||
| unzip -q -o "$zip_file" -d "${SCRIPTS_DIR}" |
There was a problem hiding this comment.
The download_binaries function downloads a zip from the third-party repo second-state/x402-agent-tools and immediately extracts it into SCRIPTS_DIR using a floating releases/latest URL, with no checksum or signature verification. If that GitHub repository or its release artifacts are compromised, this script will transparently install and execute attacker-controlled binaries on the host with the user’s privileges. To reduce supply-chain risk, pin downloads to an immutable version (tag/commit or asset checksum) and verify integrity before extraction and execution.
| git clone --depth 1 https://github.com/second-state/x402-agent-tools.git /tmp/x402-repo | ||
| cp -r /tmp/x402-repo/skill "$SKILL_DIR" | ||
| rm -rf /tmp/x402-repo |
There was a problem hiding this comment.
The Quick Install instructions clone https://github.com/second-state/x402-agent-tools.git with --depth 1 and copy the skill/ directory directly into the user’s skills path, without pinning to a specific tag/commit or verifying the contents. Because this pulls code from an unpinned third-party repository, a compromise or malicious change to that repo could cause new installs to run attacker-controlled scripts with the user’s privileges. To harden the supply chain, pin the clone to an immutable revision and/or validate expected checksums or signatures before using the downloaded files.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Summary
skill.mdtoskill/skill.mdbootstrap.shfor lazy-loading binaries from GitHub releasescreate-wallet,get-address,pay,x402curl,x402-config)skill/scripts/directory for downloaded binaries (gitignored)install.mdwith new installation instructionsHow Bootstrap Works
skill/detect the platform (linux/darwin/windows + x86_64/aarch64)skill/scripts/New Directory Structure
Test plan
Generated with Claude Code