GitLab REST API via curl. Use this skill to manage projects, issues, merge requests, and pipelines in GitLab.
/plugin marketplace add vm0-ai/api0/plugin install api0@api0This skill inherits all available tools. When active, it can use any tool Claude has access to.
Use the GitLab REST API via direct curl calls to manage projects, issues, merge requests, and pipelines.
Official docs:
https://docs.gitlab.com/ee/api/
Use this skill when you need to:
api scopeexport GITLAB_HOST="gitlab.com" # Or your self-hosted GitLab domain
export GITLAB_TOKEN="glpat-xxxxxxxxxxxx" # Personal access token with api scope
GitLab.com has rate limits of ~2000 requests per minute for authenticated users. Self-hosted instances may vary.
Important: When using
$VARin a command that pipes to another command, wrap the command containing$VARinbash -c '...'. Due to a Claude Code bug, environment variables are silently cleared when pipes are used directly.bash -c 'curl -s "https://api.example.com" -H "Authorization: Bearer $API_KEY"' | jq .
All examples below assume GITLAB_HOST and GITLAB_TOKEN are set.
Base URL: https://${GITLAB_HOST}/api/v4
Note: Project IDs can be numeric (e.g.,
123) or URL-encoded paths (e.g.,mygroup%2Fmyproject).
Verify your authentication:
bash -c 'curl -s "https://${GITLAB_HOST}/api/v4/user" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}"' | jq '{id, username, name, email, state}
Get projects accessible to you:
bash -c 'curl -s "https://${GITLAB_HOST}/api/v4/projects?membership=true&per_page=20" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}"' | jq '.[] | {id, path_with_namespace, visibility, default_branch}
Filter options:
membership=true - Only projects you're a member ofowned=true - Only projects you ownsearch=keyword - Search by namevisibility=public|internal|private - Filter by visibilityGet details for a specific project:
PROJECT_ID="123" # or URL-encoded path like "mygroup%2Fmyproject"
bash -c 'curl -s "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}"' | jq '{id, name, path_with_namespace, default_branch, visibility, web_url}
Get issues for a project:
PROJECT_ID="123"
bash -c 'curl -s "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/issues?state=opened&per_page=20" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}"' | jq '.[] | {iid, title, state, author: .author.username, labels, web_url}
Filter options:
state=opened|closed|all - Filter by statelabels=bug,urgent - Filter by labelsassignee_id=123 - Filter by assigneesearch=keyword - Search in title/descriptionGet a specific issue:
PROJECT_ID="123"
ISSUE_IID="1" # Issue internal ID (not global ID)
bash -c 'curl -s "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/issues/${ISSUE_IID}" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}"' | jq '{iid, title, description, state, author: .author.username, assignees: [.assignees[].username], labels, created_at, web_url}
Create a new issue in a project:
PROJECT_ID="123"
bash -c 'curl -s -X POST "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/issues" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" --header "Content-Type: application/json" -d '"'"'{
"title": "Bug: Login page not loading",
"description": "The login page shows a blank screen on mobile devices.",
"labels": "bug,frontend"
}'"'"' | jq '"'"'{iid, title, web_url}'"'"''
Create issue with additional fields:
PROJECT_ID="123"
ASSIGNEE_ID="456"
MILESTONE_ID="1"
curl -s -X POST "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/issues" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" --header "Content-Type: application/json" -d "{
\"title\": \"Implement user profile page\",
\"description\": \"Create a user profile page with avatar and bio.\",
\"assignee_ids\": [${ASSIGNEE_ID}],
\"milestone_id\": ${MILESTONE_ID},
\"labels\": \"feature,frontend\"
}" | jq '{iid, title, web_url}'
Update an existing issue:
PROJECT_ID="123"
ISSUE_IID="1"
bash -c 'curl -s -X PUT "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/issues/${ISSUE_IID}" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" --header "Content-Type: application/json" -d '"'"'{
"title": "Updated: Bug fix for login page",
"labels": "bug,frontend,in-progress"
}'"'"' | jq '"'"'{iid, title, labels, updated_at}'"'"''
Close an issue:
PROJECT_ID="123"
ISSUE_IID="1"
bash -c 'curl -s -X PUT "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/issues/${ISSUE_IID}" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" --header "Content-Type: application/json" -d '"'"'{"state_event": "close"}'"'"'' | jq '{iid, title, state}
Use "state_event": "reopen" to reopen a closed issue.
Add a note/comment to an issue:
PROJECT_ID="123"
ISSUE_IID="1"
bash -c 'curl -s -X POST "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/issues/${ISSUE_IID}/notes" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" --header "Content-Type: application/json" -d '"'"'{"body": "Investigating this issue. Will update soon."}'"'"'' | jq '{id, body, author: .author.username, created_at}
Get merge requests for a project:
PROJECT_ID="123"
bash -c 'curl -s "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/merge_requests?state=opened&per_page=20" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}"' | jq '.[] | {iid, title, state, source_branch, target_branch, author: .author.username, web_url}
Filter options:
state=opened|closed|merged|all - Filter by statescope=created_by_me|assigned_to_me|all - Filter by involvementlabels=review-needed - Filter by labelsGet a specific merge request:
PROJECT_ID="123"
MR_IID="1"
bash -c 'curl -s "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/merge_requests/${MR_IID}" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}"' | jq '{iid, title, state, source_branch, target_branch, author: .author.username, merge_status, has_conflicts, web_url}
Create a new merge request:
PROJECT_ID="123"
bash -c 'curl -s -X POST "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/merge_requests" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" --header "Content-Type: application/json" -d '"'"'{
"source_branch": "feature/user-profile",
"target_branch": "main",
"title": "Add user profile page",
"description": "This MR adds a new user profile page with avatar support."
}'"'"' | jq '"'"'{iid, title, web_url}'"'"''
Merge an MR (if it's ready):
PROJECT_ID="123"
MR_IID="1"
bash -c 'curl -s -X PUT "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/merge_requests/${MR_IID}/merge" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" --header "Content-Type: application/json" -d '"'"'{"merge_when_pipeline_succeeds": true}'"'"'' | jq '{iid, title, state, merged_by: .merged_by.username}
Options:
merge_when_pipeline_succeeds=true - Auto-merge when pipeline passessquash=true - Squash commits before mergingshould_remove_source_branch=true - Delete source branch after mergeGet pipelines for a project:
PROJECT_ID="123"
bash -c 'curl -s "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/pipelines?per_page=10" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}"' | jq '.[] | {id, status, ref, sha: .sha[0:8], created_at, web_url}
Get details of a specific pipeline:
PROJECT_ID="123"
PIPELINE_ID="456"
bash -c 'curl -s "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/pipelines/${PIPELINE_ID}" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}"' | jq '{id, status, ref, duration, finished_at, web_url}
Get jobs in a pipeline:
PROJECT_ID="123"
PIPELINE_ID="456"
bash -c 'curl -s "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/pipelines/${PIPELINE_ID}/jobs" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}"' | jq '.[] | {id, name, stage, status, duration}
Search for users:
bash -c 'curl -s -G "https://${GITLAB_HOST}/api/v4/users" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" --data-urlencode "search=john"' | jq '.[] | {id, username, name, state}
Create a new project:
bash -c 'curl -s -X POST "https://${GITLAB_HOST}/api/v4/projects" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" --header "Content-Type: application/json" -d '"'"'{
"name": "my-new-project",
"visibility": "private",
"initialize_with_readme": true
}'"'"' | jq '"'"'{id, path_with_namespace, web_url}'"'"''
Delete an issue (requires admin or owner permissions):
PROJECT_ID="123"
ISSUE_IID="1"
curl -s -X DELETE "https://${GITLAB_HOST}/api/v4/projects/${PROJECT_ID}/issues/${ISSUE_IID}" --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" -w "\nHTTP Status: %{http_code}"
Returns 204 No Content on success.
When using project paths instead of numeric IDs, URL-encode the path:
mygroup/myproject → mygroup%2Fmyprojectmygroup/subgroup/myproject → mygroup%2Fsubgroup%2Fmyproject# Using numeric ID
PROJECT_ID="123"
# Using encoded path
PROJECT_ID="mygroup%2Fmyproject"
iid (internal ID like #1, #2) not the global id%2Fper_page and page params for large result setsmerge_status is can_be_mergedGITLAB_HOST to your instance domain (without https://)