surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic · github.com/anthropics
- Create GitHub API caching script that handles authenticated and unauthenticated requests - Update Dockerfile to include the script in the container - Update init-firewall.sh to use cached GitHub API data - Modify devcontainer.json to run cache script before build and mount cache directory 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
| M.devcontainer/Dockerfile |
| A.devcontainer/cache-github-api.sh |
| M.devcontainer/devcontainer.json |
| M.devcontainer/init-firewall.sh |
| @@ -69,10 +69,10 @@ | ||
| 69 | 69 | # Install Claude |
| 70 | 70 | RUN npm install -g @anthropic-ai/claude-code |
| 71 | 71 | |
| 72 | -# Copy and set up firewall script | |
| 73 | -COPY init-firewall.sh /usr/local/bin/ | |
| 72 | +# Copy and set up scripts | |
| 73 | +COPY init-firewall.sh cache-github-api.sh /usr/local/bin/ | |
| 74 | 74 | USER root |
| 75 | -RUN chmod +x /usr/local/bin/init-firewall.sh && \ | |
| 75 | +RUN chmod +x /usr/local/bin/init-firewall.sh /usr/local/bin/cache-github-api.sh && \ | |
| 76 | 76 | echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \ |
| 77 | 77 | chmod 0440 /etc/sudoers.d/node-firewall |
| 78 | 78 | USER node |
| @@ -1,0 +1,109 @@ | ||
| 1 | +#!/bin/bash | |
| 2 | +set -e | |
| 3 | + | |
| 4 | +# Script to cache GitHub API data | |
| 5 | +# Used to prevent rate limiting during container builds | |
| 6 | + | |
| 7 | +# Configuration | |
| 8 | +# Store cache in the home directory | |
| 9 | +CACHE_DIR="${HOME}/.github-meta-cache" | |
| 10 | +CACHE_FILE="${CACHE_DIR}/meta.json" | |
| 11 | +TIMESTAMP_FILE="${CACHE_DIR}/meta-timestamp.txt" | |
| 12 | +MAX_AGE_SECONDS=3600 # Cache expires after 1 hour | |
| 13 | + | |
| 14 | +# Create cache directory if it doesn't exist | |
| 15 | +mkdir -p "${CACHE_DIR}" | |
| 16 | + | |
| 17 | +# Function to get current timestamp | |
| 18 | +get_timestamp() { | |
| 19 | + date +%s | |
| 20 | +} | |
| 21 | + | |
| 22 | +# Function to check if cache is valid | |
| 23 | +is_cache_valid() { | |
| 24 | + if [[ ! -f "${CACHE_FILE}" || ! -f "${TIMESTAMP_FILE}" ]]; then | |
| 25 | + return 1 | |
| 26 | + fi | |
| 27 | + | |
| 28 | + local cache_time=$(cat "${TIMESTAMP_FILE}") | |
| 29 | + local current_time=$(get_timestamp) | |
| 30 | + local age=$((current_time - cache_time)) | |
| 31 | + | |
| 32 | + if [[ ${age} -gt ${MAX_AGE_SECONDS} ]]; then | |
| 33 | + echo "Cache is expired (${age} seconds old)" | |
| 34 | + return 1 | |
| 35 | + fi | |
| 36 | + | |
| 37 | + echo "Using cached GitHub API data (${age} seconds old)" | |
| 38 | + return 0 | |
| 39 | +} | |
| 40 | + | |
| 41 | +# Function to fetch data using authenticated gh cli | |
| 42 | +fetch_with_gh() { | |
| 43 | + echo "Attempting to fetch GitHub API data using authenticated gh CLI..." | |
| 44 | + if gh auth status &>/dev/null; then | |
| 45 | + gh api meta > "${CACHE_FILE}" && | |
| 46 | + get_timestamp > "${TIMESTAMP_FILE}" && | |
| 47 | + echo "Successfully fetched and cached GitHub API data using gh CLI" | |
| 48 | + return $? | |
| 49 | + else | |
| 50 | + echo "gh CLI not authenticated" | |
| 51 | + return 1 | |
| 52 | + fi | |
| 53 | +} | |
| 54 | + | |
| 55 | +# Function to fetch data using curl | |
| 56 | +fetch_with_curl() { | |
| 57 | + echo "Attempting to fetch GitHub API data using curl..." | |
| 58 | + # First try with GITHUB_TOKEN if available | |
| 59 | + if [[ -n "${GITHUB_TOKEN}" ]]; then | |
| 60 | + echo "Using GITHUB_TOKEN for authentication" | |
| 61 | + curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/meta > "${CACHE_FILE}" && | |
| 62 | + get_timestamp > "${TIMESTAMP_FILE}" && | |
| 63 | + echo "Successfully fetched and cached GitHub API data using curl with token" | |
| 64 | + return $? | |
| 65 | + else | |
| 66 | + # Fall back to unauthenticated request | |
| 67 | + echo "No GITHUB_TOKEN found, making unauthenticated request (may be rate limited)" | |
| 68 | + curl -s https://api.github.com/meta > "${CACHE_FILE}" | |
| 69 | + | |
| 70 | + # Check if the response indicates rate limiting | |
| 71 | + if grep -q "API rate limit exceeded" "${CACHE_FILE}"; then | |
| 72 | + echo "Rate limit exceeded for unauthenticated request" | |
| 73 | + return 1 | |
| 74 | + else | |
| 75 | + get_timestamp > "${TIMESTAMP_FILE}" | |
| 76 | + echo "Successfully fetched and cached GitHub API data using curl without auth" | |
| 77 | + return 0 | |
| 78 | + fi | |
| 79 | + fi | |
| 80 | +} | |
| 81 | + | |
| 82 | +# Main logic | |
| 83 | +if is_cache_valid; then | |
| 84 | + echo "Using existing cache from $(cat ${TIMESTAMP_FILE})" | |
| 85 | + exit 0 | |
| 86 | +fi | |
| 87 | + | |
| 88 | +# Try with gh CLI first | |
| 89 | +if ! fetch_with_gh; then | |
| 90 | + # Fall back to curl | |
| 91 | + if ! fetch_with_curl; then | |
| 92 | + # Both methods failed, check if we have an existing cache file | |
| 93 | + if [[ -f "${CACHE_FILE}" ]]; then | |
| 94 | + echo "Warning: Failed to update cache, using existing cached data (which may be expired)" | |
| 95 | + exit 0 | |
| 96 | + else | |
| 97 | + echo "Error: Failed to fetch GitHub API data and no cache exists" | |
| 98 | + exit 1 | |
| 99 | + fi | |
| 100 | + fi | |
| 101 | +fi | |
| 102 | + | |
| 103 | +# Display a summary of the cached data | |
| 104 | +echo "GitHub API meta data cached successfully. Summary:" | |
| 105 | +jq -r '.domains.actions | length' "${CACHE_FILE}" > /dev/null 2>&1 && | |
| 106 | + echo "- Actions domains: $(jq -r '.domains.actions | length' "${CACHE_FILE}")" || | |
| 107 | + echo "- Could not parse actions domains from cache file" | |
| 108 | + | |
| 109 | +exit 0 |
| @@ -4,6 +4,9 @@ | ||
| 4 | 4 | "dockerfile": "Dockerfile", |
| 5 | 5 | "args": { |
| 6 | 6 | "TZ": "${localEnv:TZ:America/Los_Angeles}" |
| 7 | + }, | |
| 8 | + "prebuild": { | |
| 9 | + "command": "bash -c '${localWorkspaceFolder}/.devcontainer/cache-github-api.sh || echo \"Warning: Failed to cache GitHub API data\"'" | |
| 7 | 10 | } |
| 8 | 11 | }, |
| 9 | 12 | "runArgs": [ |
| @@ -39,7 +42,8 @@ | ||
| 39 | 42 | "remoteUser": "node", |
| 40 | 43 | "mounts": [ |
| 41 | 44 | "source=claude-code-bashhistory,target=/commandhistory,type=volume", |
| 42 | - "source=claude-code-config,target=/home/node/.claude,type=volume" | |
| 45 | + "source=claude-code-config,target=/home/node/.claude,type=volume", | |
| 46 | + "type=bind,source=${localEnv:HOME}/.github-meta-cache,target=/github-meta-cache,consistency=cached" | |
| 43 | 47 | ], |
| 44 | 48 | "remoteEnv": { |
| 45 | 49 | "NODE_OPTIONS": "--max-old-space-size=4096", |
| @@ -27,16 +27,20 @@ | ||
| 27 | 27 | # Create ipset with CIDR support |
| 28 | 28 | ipset create allowed-domains hash:net |
| 29 | 29 | |
| 30 | -# Fetch GitHub meta information and aggregate + add their IP ranges | |
| 31 | -echo "Fetching GitHub IP ranges..." | |
| 32 | -gh_ranges=$(curl -s https://api.github.com/meta) | |
| 33 | -if [ -z "$gh_ranges" ]; then | |
| 34 | - echo "ERROR: Failed to fetch GitHub IP ranges" | |
| 35 | - exit 1 | |
| 36 | -fi | |
| 30 | +# Use cached GitHub meta information from mounted volume | |
| 31 | +CACHE_FILE="/github-meta-cache/meta.json" | |
| 37 | 32 | |
| 38 | -if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then | |
| 39 | - echo "ERROR: GitHub API response missing required fields" | |
| 33 | +echo "Using cached GitHub IP ranges..." | |
| 34 | +if [ -f "${CACHE_FILE}" ]; then | |
| 35 | + gh_ranges=$(cat "${CACHE_FILE}") | |
| 36 | + | |
| 37 | + # Verify the cached data is valid | |
| 38 | + if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then | |
| 39 | + echo "ERROR: Cached GitHub API data is invalid" | |
| 40 | + exit 1 | |
| 41 | + fi | |
| 42 | +else | |
| 43 | + echo "ERROR: No cached GitHub IP ranges found" | |
| 40 | 44 | exit 1 |
| 41 | 45 | fi |
| 42 | 46 |