Install
openclaw skills install @gaolfun/github-integrationManage GitHub repositories by creating and updating issues, pull requests, files, labels, and performing code searches using GitHub REST API with a personal...
openclaw skills install @gaolfun/github-integrationThis skill wraps the GitHub REST API (v3) using curl commands executed in a shell. It handles:
All requests use Bearer token authentication with a GitHub Personal Access Token (PAT). The base URL is https://api.github.com.
issues: Read and writepull_requests: Read and writecontents: Read and writesearch: Read and writemetadata: Read-only (always included)Store the PAT in your OpenClaw environment so it is never hard-coded:
# Option A — export in your shell profile (~/.bashrc, ~/.zshrc)
export GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
# Option B — write to a secure file with restricted permissions
echo "ghp_xxxxxxxxxxxxxxxxxxxx" > ~/.config/openclaw/github_token
chmod 600 ~/.config/openclaw/github_token
Reference it in every curl call:
GH_TOKEN="$(cat ~/.config/openclaw/github_token)"
Many examples use jq to parse JSON responses. Install it if not present:
sudo apt install jq # Debian/Ubuntu
brew install jq # macOS
Create a new issue in a repository.
POST /repos/{owner}/{repo}/issues
Trigger phrases:
List issues (open, closed, or all) in a repository.
GET /repos/{owner}/{repo}/issues
Trigger phrases:
Fetch a specific issue by number.
GET /repos/{owner}/{repo}/issues/{issue_number}
Update issue fields (state, title, body, labels, assignee).
PATCH /repos/{owner}/{repo}/issues/{issue_number}
Trigger phrases:
Add a comment to an issue or pull request.
POST /repos/{owner}/{repo}/issues/{issue_number}/comments
Trigger phrases:
GET /repos/{owner}/{repo}/issues/{issue_number}/comments
Create a new pull request between two branches.
POST /repos/{owner}/{repo}/pulls
Trigger phrases:
GET /repos/{owner}/{repo}/pulls
Trigger phrases:
GET /repos/{owner}/{repo}/pulls/{pull_number}
PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge
Retrieve the content of a file (returns base64-encoded content + sha).
GET /repos/{owner}/{repo}/contents/{path}
Trigger phrases:
Create a new file or update an existing one. Requires the file's current sha for updates.
PUT /repos/{owner}/{repo}/contents/{path}
Trigger phrases:
Delete a file. Requires the file's current sha and a commit message.
DELETE /repos/{owner}/{repo}/contents/{path}
Search across repositories using GitHub's search API.
GET /search/code?q={query}&repo={owner}/{repo}
Trigger phrases:
GET /search/repositories?q={query}
Trigger phrases:
GET /repos/{owner}/{repo}
Trigger phrases:
GET /repos/{owner}/{repo}/labels
POST /repos/{owner}/{repo}/labels
POST /repos/{owner}/{repo}/issues/{issue_number}/labels
Trigger phrases:
DELETE /repos/{owner}/{repo}/issues/{issue_number}/labels/{name}
# Set once per session or per command block
GH_TOKEN="$(cat ~/.config/openclaw/github_token)"
BASE_URL="https://api.github.com"
HEADERS="-H 'Authorization: Bearer ${GH_TOKEN}' -H 'Accept: application/vnd.github+json'"
# Example usage:
# curl -s ${HEADERS} ${BASE_URL}/repos/owner/repo
OWNER="octocat"
REPO="hello-world"
URL="${BASE_URL}/repos/${OWNER}/${REPO}/issues"
curl -s -X POST ${URL} \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
-d '{
"title": "Bug: login fails with special characters",
"body": "Steps to reproduce:\n1. Enter password with @ symbol\n2. Click login\nExpected: success; Actual: 500 error",
"labels": ["bug", "priority-high"],
"assignees": ["octocat"]
}' | jq '{ number, title, state, html_url }'
Response fields used: number, title, state, html_url, labels
OWNER="torvalds"
REPO="linux"
STATE="open" # open | closed | all
PER_PAGE="10"
URL="${BASE_URL}/repos/${OWNER}/${REPO}/issues?state=${STATE}&per_page=${PER_PAGE}"
curl -s ${URL} \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" | \
jq '.[] | { number, title, state, labels: [.labels[].name], created_at }'
OWNER="facebook"
REPO="react"
ISSUE_NUM="25000"
URL="${BASE_URL}/repos/${OWNER}/${REPO}/issues/${ISSUE_NUM}"
curl -s ${URL} \
-H "Authorization: Bearer ${GH_TOKEN}" | \
jq '{ number, title, state, body, labels: [.labels[].name], assignee, comments, html_url }'
OWNER="octocat"
REPO="hello-world"
ISSUE_NUM="42"
URL="${BASE_URL}/repos/${OWNER}/${REPO}/issues/${ISSUE_NUM}"
curl -s -X PATCH ${URL} \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d '{ "state": "closed", "state_reason": "completed" }' | \
jq '{ number, state, state_reason }'
OWNER="microsoft"
REPO="vscode"
ISSUE_NUM="12345"
COMMENT_BODY="Thanks for reporting this! I can reproduce it. Labeling as bug."
curl -s -X POST \
"${BASE_URL}/repos/${OWNER}/${REPO}/issues/${ISSUE_NUM}/comments" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d "$(jq -n --arg body "$COMMENT_BODY" '{ body: $body }')" | \
jq '{ id, body, created_at, html_url }'
OWNER="microsoft"
REPO="vscode"
ISSUE_NUM="12345"
curl -s "${BASE_URL}/repos/${OWNER}/${REPO}/issues/${ISSUE_NUM}/comments" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
jq '.[] | { id, user: .user.login, body, created_at }'
OWNER="myorg"
REPO="myrepo"
URL="${BASE_URL}/repos/${OWNER}/${REPO}/pulls"
curl -s -X POST ${URL} \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d '{
"title": "feat: add dark mode support",
"head": "feature/dark-mode",
"base": "main",
"body": "## Summary\nAdds a dark mode toggle to the settings page.\n\n## Test plan\n- [ ] Toggle works on all pages\n- [ ] Preference persists across sessions",
"draft": false
}' | jq '{ number, title, state, html_url, draft }'
OWNER="torvalds"
REPO="linux"
STATE="open" # open | closed | all | merged
curl -s "${BASE_URL}/repos/${OWNER}/${REPO}/pulls?state=${STATE}&per_page=20" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
jq '.[] | { number, title, state, user: .user.login, head: .head.ref, base: .base.ref }'
OWNER="myorg"
REPO="myrepo"
PR_NUM="42"
METHOD="squash" # merge | squash | rebase
curl -s -X PUT \
"${BASE_URL}/repos/${OWNER}/${REPO}/pulls/${PR_NUM}/merge" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d '{
"merge_method": "'"${METHOD}"'",
"commit_title": "feat: add dark mode (squashed)",
"commit_message": "See PR description"
}' | jq '{ merged, message }'
OWNER="torvalds"
REPO="linux"
FILE_PATH="README"
curl -s "${BASE_URL}/repos/${OWNER}/${REPO}/contents/${FILE_PATH}" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
jq '{ name, path, sha, size, html_url, content: (.content | gsub("\n"; "")) }'
Note: The
contentfield is base64-encoded. To decode:bash CONTENT=$(curl -s "${BASE_URL}/repos/${OWNER}/${REPO}/contents/${FILE_PATH}" \ -H "Authorization: Bearer ${GH_TOKEN}" | jq -r '.content') echo "$CONTENT" | base64 -d
OWNER="myorg"
REPO="myrepo"
FILE_PATH="docs/new-feature.md"
COMMIT_MSG="docs: add new feature documentation"
CONTENT=$(echo -n "# New Feature\n\nThis feature does X." | base64 -w0)
curl -s -X PUT \
"${BASE_URL}/repos/${OWNER}/${REPO}/contents/${FILE_PATH}" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d "{
\"message\": \"${COMMIT_MSG}\",
\"content\": \"${CONTENT}\"
}" | jq '{ commit: { sha, message, html_url } }'
To update a file, you must first fetch its current SHA.
OWNER="myorg"
REPO="myrepo"
FILE_PATH="README.md"
COMMIT_MSG="docs: update README with new screenshot"
# Step 1 — get current SHA
SHA=$(curl -s "${BASE_URL}/repos/${OWNER}/${REPO}/contents/${FILE_PATH}" \
-H "Authorization: Bearer ${GH_TOKEN}" | jq -r '.sha')
# Step 2 — PUT with the SHA
NEW_CONTENT=$(echo -n "Updated README content here" | base64 -w0)
curl -s -X PUT \
"${BASE_URL}/repos/${OWNER}/${REPO}/contents/${FILE_PATH}" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d "{
\"message\": \"${COMMIT_MSG}\",
\"content\": \"${NEW_CONTENT}\",
\"sha\": \"${SHA}\"
}" | jq '{ commit: { sha, message } }'
OWNER="myorg"
REPO="myrepo"
FILE_PATH="old/deprecated-file.txt"
COMMIT_MSG="chore: remove deprecated file"
# Get SHA first
SHA=$(curl -s "${BASE_URL}/repos/${OWNER}/${REPO}/contents/${FILE_PATH}" \
-H "Authorization: Bearer ${GH_TOKEN}" | jq -r '.sha')
curl -s -X DELETE \
"${BASE_URL}/repos/${OWNER}/${REPO}/contents/${FILE_PATH}" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d "{
\"message\": \"${COMMIT_MSG}\",
\"sha\": \"${SHA}\"
}" | jq '{ commit: { sha, message } }'
QUERY="csrf token"
OWNER="myorg"
REPO="myrepo"
PER_PAGE="30"
# URL-encode the query string
ENCODED_QUERY=$(echo -n "${QUERY}" | jq -sRr @uri)
curl -s "https://api.github.com/search/code?q=${ENCODED_QUERY}+repo:${OWNER}/${REPO}&per_page=${PER_PAGE}" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" | \
jq '{ total_count, items: [.items[] | { name, path, html_url }] }'
QUERY="machine learning python"
SORT="stars" # stars | forks | updated
ORDER="desc"
PER_PAGE="10"
ENCODED_QUERY=$(echo -n "${QUERY}" | jq -sRr @uri)
curl -s "https://api.github.com/search/repositories?q=${ENCODED_QUERY}&sort=${SORT}&order=${ORDER}&per_page=${PER_PAGE}" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
jq '.items[] | { full_name, description, stargazers_count, forks_count, language, html_url }'
OWNER="torvalds"
REPO="linux"
curl -s "${BASE_URL}/repos/${OWNER}/${REPO}" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
jq '{
full_name, description, homepage,
stargazers_count, forks_count, open_issues_count,
language, default_branch, visibility,
created_at, updated_at, pushed_at,
topics: [.topics[0:10]],
license: .license.spdx_id
}'
OWNER="microsoft"
REPO="vscode"
curl -s "${BASE_URL}/repos/${OWNER}/${REPO}/labels?per_page=100" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
jq '.[] | { name, color, description }'
OWNER="myorg"
REPO="myrepo"
curl -s -X POST \
"${BASE_URL}/repos/${OWNER}/${REPO}/labels" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d '{
"name": "priority-critical",
"color": "d73a4a",
"description": "Critical priority, resolve immediately"
}' | jq '{ name, color, description }'
OWNER="myorg"
REPO="myrepo"
ISSUE_NUM="42"
curl -s -X POST \
"${BASE_URL}/repos/${OWNER}/${REPO}/issues/${ISSUE_NUM}/labels" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d "$(jq -n --arg name "bug" '{ labels: [$name] }')" | \
jq '.[] | { name, color }'
To add multiple labels at once:
curl -s -X POST \
"${BASE_URL}/repos/${OWNER}/${REPO}/issues/${ISSUE_NUM}/labels" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d '{
"labels": ["bug", "priority-high", "needs-triage"]
}' | jq '.'
OWNER="myorg"
REPO="myrepo"
ISSUE_NUM="42"
LABEL_NAME="needs-triage"
# URL-encode the label name (e.g., spaces become %20)
ENCODED_LABEL=$(echo -n "${LABEL_NAME}" | jq -sRr @uri)
curl -s -X DELETE \
"${BASE_URL}/repos/${OWNER}/${REPO}/issues/${ISSUE_NUM}/labels/${ENCODED_LABEL}" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
jq -e '.name' || echo "Label removed (or already absent)"
{
"ok": true,
"action": "create_issue",
"data": {
"number": 42,
"title": "Bug: login fails",
"state": "open",
"html_url": "https://github.com/owner/repo/issues/42"
}
}
{
"ok": false,
"action": "api_request",
"error": "rate_limited",
"message": "GitHub API rate limit exceeded. Retry after 3600 seconds.",
"retry_after": 3600
}
{
"ok": false,
"action": "get_file",
"error": "not_found",
"message": "File not found: path/to/file.md in owner/repo"
}
{
"ok": false,
"error": "missing_token",
"message": "GITHUB_TOKEN is not set. Provide a GitHub PAT and store it in ~/.config/openclaw/github_token"
}
User: "Can you open a GitHub issue on octocat/hello-world? Title: 'Login breaks with @ in password'. Body: 'Steps: 1. Enter @ in password. 2. Click login. Expected: success. Actual: 500 error.' Add labels: bug, priority-high."
Assistant (executes):
GH_TOKEN="$(cat ~/.config/openclaw/github_token)"
curl -s -X POST "https://api.github.com/repos/octocat/hello-world/issues" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d '{
"title": "Login breaks with @ in password",
"body": "Steps: 1. Enter @ in password. 2. Click login.\nExpected: success. Actual: 500 error.",
"labels": ["bug", "priority-high"]
}' | jq '{ number, title, state, html_url, labels: [.labels[].name] }'
Output:
{
"number": 42,
"title": "Login breaks with @ in password",
"state": "open",
"html_url": "https://github.com/octocat/hello-world/issues/42",
"labels": ["bug", "priority-high"]
}
Assistant: Done! Issue #42 created: https://github.com/octocat/hello-world/issues/42
Labels: bug, priority-high.
User: "Get the current README.md from myorg/myrepo, then update it to add a '## Changelog' section."
Assistant (step 1 — get current contents + sha):
GH_TOKEN="$(cat ~/.config/openclaw/github_token)"
curl -s "https://api.github.com/repos/myorg/myrepo/contents/README.md" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
jq '{ sha, content: (.content | gsub("\n"; "")), encoding }'
Output:
{
"sha": "a1b2c3d4e5f6789012345678901234567890abcd",
"content": "IyBNeVJlcG8KClRoaXMgaXMgdGhlIFJFQURNRSB...",
"encoding": "base64"
}
Assistant (step 2 — decode, append changelog, encode, PUT):
GH_TOKEN="$(cat ~/.config/openclaw/github_token)"
SHA="a1b2c3d4e5f6789012345678901234567890abcd"
OLD_CONTENT=$(curl -s "https://api.github.com/repos/myorg/myrepo/contents/README.md" \
-H "Authorization: Bearer ${GH_TOKEN}" | jq -r '.content' | base64 -d)
NEW_CONTENT="${OLD_CONTENT}
## Changelog
- 2026-07-04: Added changelog section"
ENCODED=$(echo -n "${NEW_CONTENT}" | base64 -w0)
curl -s -X PUT "https://api.github.com/repos/myorg/myrepo/contents/README.md" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d "{
\"message\": \"docs: add changelog section\",
\"content\": \"${ENCODED}\",
\"sha\": \"${SHA}\"
}" | jq '{ commit: { sha, message } }'
Output:
{ "commit": { "sha": "newcommitsha1234567890", "message": "docs: add changelog section" } }
User: "Search for 'csrf' in the myorg/frontend repo, then open a PR to fix it on a branch called fix/csrf-vulnerability from main."
Assistant (search):
GH_TOKEN="$(cat ~/.config/openclaw/github_token)"
QUERY="csrf"
curl -s "https://api.github.com/search/code?q=csrf+repo:myorg/frontend&per_page=10" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
jq '{ total_count, files: [.items[] | { path, html_url }] }'
Output:
{ "total_count": 3, "files": [...] }
Assistant (create PR):
GH_TOKEN="$(cat ~/.config/openclaw/github_token)"
curl -s -X POST "https://api.github.com/repos/myorg/frontend/pulls" \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-d '{
"title": "fix: address CSRF vulnerability in auth flow",
"head": "fix/csrf-vulnerability",
"base": "main",
"body": "## Summary\nFixes CSRF vulnerability found in search. Adds token validation.\n\n## Changes\n- Added CSRF token check in POST handlers\n- Added unit tests for auth flow\n\n## References\nCloses #42"
}' | jq '{ number, title, state, html_url, draft }'
Output:
{ "number": 99, "title": "fix: address CSRF vulnerability in auth flow", "state": "open", "html_url": "https://github.com/myorg/frontend/pull/99", "draft": false }
Assistant: Found 3 files with "csrf". PR #99 opened: https://github.com/myorg/frontend/pull/99 — ready for review.
openclaw-github-skill# Create the config directory
mkdir -p ~/.config/openclaw
# Write the token (replace with your actual token)
echo "ghp_xxxxxxxxxxxxxxxxxxxx" > ~/.config/openclaw/github_token
# Lock down the file
chmod 600 ~/.config/openclaw/github_token
# Verify it works
cat ~/.config/openclaw/github_token
GH_TOKEN="$(cat ~/.config/openclaw/github_token)"
curl -s -H "Authorization: Bearer ${GH_TOKEN}" \
"https://api.github.com/user" | \
jq '{ login, name, email }'
Expected output shows your GitHub username and email. If you get 401 Unauthorized, the token is invalid or lacks the correct scopes.
sudo apt update && sudo apt install -y jq
# Test listing issues (public repo)
GH_TOKEN="$(cat ~/.config/openclaw/github_token)"
curl -s "https://api.github.com/repos/octocat/hello-world/issues?per_page=5" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
jq '.[] | { number, title }'
If your OpenClaw instance uses environment variables, add this to ~/.bashrc or the OpenClaw service environment:
export GITHUB_TOKEN="$(cat ~/.config/openclaw/github_token)"
Restart the OpenClaw gateway after adding the variable.
| Endpoint type | Authenticated limit | Notes |
|---|---|---|
| Most API endpoints | 5,000 requests/hour | Per token, per API instance |
| Search (code/repos) | 30 requests/minute | Subject to secondary limit |
| GraphQL API | 5,000 points/hour | Different from REST limits |
Handle rate limits in scripts:
# Check remaining requests
REMAINING=$(curl -s -I "https://api.github.com/rate_limit" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
grep -i "x-ratelimit-remaining" | awk '{print $2}')
if [ "$REMAINING" -lt 10 ]; then
echo "Warning: only ${REMAINING} requests remaining. Waiting..."
sleep 60
fi
When rate limited (HTTP 429):
# Parse Retry-After header (seconds)
RETRY_AFTER=$(curl -s -I "https://api.github.com/repos/owner/repo" \
-H "Authorization: Bearer ${GH_TOKEN}" | \
grep -i "retry-after" | awk '{print $2}')
echo "Rate limited. Retry after ${RETRY_AFTER} seconds."
# Consider sleeping + retrying
contents:write needed.repo scope needed for public repos: Public repo operations work with limited-scope fine-grained tokens.repo (classic) scope unless you need full repo access — it grants access to all your private repos.-w0 flag with base64).CONTENT=$(echo "$CONTENT" | tr -d '\n')
echo "$CONTENT" | base64 -d
sha of that file.409 Conflict."draft": true when creating a PR to open it as a draft.curl -d with single-quoted JSON works fine for simple strings.jq -n --arg key "$value" to safely embed variables without JSON-escaping issues.For large result sets, GitHub paginates at 30 (default) or 30/60/100 (max per page). Use:
# Page through issues
PAGE=1
PER_PAGE=100
while true; do
RESULT=$(curl -s "${BASE_URL}/repos/${OWNER}/${REPO}/issues?page=${PAGE}&per_page=${PER_PAGE}" \
-H "Authorization: Bearer ${GH_TOKEN}")
COUNT=$(echo "$RESULT" | jq length)
[ "$COUNT" -eq 0 ] && break
echo "$RESULT" | jq '.[] | { number, title }'
PAGE=$((PAGE + 1))
done
https://api.github.com.chmod 600 as shown in §2.# Variables
GH_TOKEN="$(cat ~/.config/openclaw/github_token)"
BASE="https://api.github.com"
H="-H 'Authorization: Bearer ${GH_TOKEN}' -H 'Accept: application/vnd.github+json'"
# Create issue
curl -s -X POST $BASE/repos/OWNER/REPO/issues -H "$H" -d '{"title":"Bug"}'
# List issues
curl -s "$BASE/repos/OWNER/REPO/issues?state=open&per_page=30" -H "$H"
# Close issue
curl -s -X PATCH $BASE/repos/OWNER/REPO/issues/N -H "$H" -d '{"state":"closed"}'
# Comment
curl -s -X POST $BASE/repos/OWNER/REPO/issues/N/comments -H "$H" -d "$(jq -n --arg b 'Comment text' '{body:$b}')"
# Create PR
curl -s -X POST $BASE/repos/OWNER/REPO/pulls -H "$H" -d '{"title":"PR","head":"branch","base":"main"}'
# Get file
curl -s $BASE/repos/OWNER/REPO/contents/PATH -H "$H"
# Update file (needs sha)
SHA=$(curl -s $BASE/repos/OWNER/REPO/contents/PATH -H "$H" | jq -r '.sha')
ENCODED=$(echo "new content" | base64 -w0)
curl -s -X PUT $BASE/repos/OWNER/REPO/contents/PATH -H "$H" -d "{\"message\":\"update\",\"content\":\"$ENCODED\",\"sha\":\"$SHA\"}"
# Search code
curl -s "https://api.github.com/search/code?q=QUERY+repo:OWNER/REPO" -H "$H"
# Get repo info
curl -s $BASE/repos/OWNER/REPO -H "$H"
# Add label
curl -s -X POST $BASE/repos/OWNER/REPO/issues/N/labels -H "$H" -d '{"labels":["bug"]}'
# Check rate limit
curl -s $BASE/rate_limit -H "$H"