Gitlab
MicrosoftOfficialPublished by Microsoft
- Skill
- Tool integration
- Software development
- MIT
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The GitLab skill provides a repository-local Python interface for interacting with GitLab.com or self-managed GitLab instances. It enables users to perform common development tasks such as listing and updating merge requests, triggering pipelines, and inspecting job logs directly from the command line. The tool supports both OAuth and legacy personal access token authentication modes and includes optional audit logging for tracking API operations.
What it does
- Authenticates via OAuth or legacy personal access tokens
- Lists, creates, and updates merge requests
- Retrieves merge request notes and comments
- Triggers pipelines for specific branches or tags
- Lists pipeline jobs and retrieves raw job logs
- Provides structured audit logging for API requests
When to use it
- When you need to automate GitLab interactions within a local development workflow
- When you prefer a command-line interface for managing merge requests and pipelines
- When you need to extract specific data fields from GitLab resources for reporting
- When you require an auditable trail of API operations for internal tracking
Advantages
- Supports concise output filtering using the --fields parameter
- Integrates with existing git repositories via automatic project detection
- Offers secure OAuth authentication with PKCE support
- Provides a structured JSON Lines audit trail for operations
Usage examples
- “List all open merge requests for the current project.”
- The CLI outputs a list of open merge requests, displaying the IID, title, and author name for each.
- “Get the job logs for pipeline 12345.”
- The CLI retrieves and prints the raw log output for the specified pipeline job.
- “Create a new merge request from a JSON payload.”
- The CLI submits the provided JSON data to create a new merge request on the configured GitLab project.
- “Add a comment to merge request 42.”
- The CLI posts the provided text as a comment on the specified merge request.
Install
8 of its files are not mirrored here, so install it from its source:
https://github.com/microsoft/hve-core/tree/f425e943bf228052a23e6bc4810b5faef030314f/.github/skills/project-planning/gitlab
Documentation by Microsoft
GitLab Skill
Overview
Use this skill to inspect and update GitLab merge requests, notes, pipelines, and job logs against GitLab.com or self-managed GitLab instances.
This skill is the repository-local Python workflow for GitLab tasks. It is not the official GitLab MCP server integration surface.
This first hve-core implementation is Python-only. Run the CLI through
python scripts/gitlab.py and prefer --fields for read operations to keep
output concise.
Prerequisites
The skill requires Python 3.11 or later.
Set these environment variables before running any command:
| Variable | Required | Example | Purpose |
|---|---|---|---|
GITLAB_URL | Yes | https://gitlab.com | GitLab instance URL |
GITLAB_AUTH_MODE | Optional | oauth | oauth or explicit legacy-token compatibility mode |
GITLAB_OAUTH_CLIENT_ID | OAuth | Application ID | Non-confidential OAuth application ID; no client secret |
GITLAB_TOKEN | Legacy mode | glpat-... | PAT accepted only with GITLAB_AUTH_MODE=legacy-token |
GITLAB_PROFILE | No | default | Named OAuth profile |
GITLAB_TOKEN_STORE | No | path | OAuth store override; parent must be owner-only on POSIX |
GITLAB_ALLOW_INSECURE | No | 1 | Permit explicit loopback HTTP development endpoints only |
XDG_DATA_HOME | No | path | POSIX data root used for the default OAuth store |
LOCALAPPDATA | No | path | Reserved Windows data root; OAuth persistence currently fails closed on Windows |
GITLAB_PROJECT | No | group/project | Project path or numeric project ID |
If GITLAB_PROJECT is not set, the script attempts to detect the project from
git remote get-url origin. Set the variable explicitly when you are not in a
git repository or when you want to target a different project.
Operational Variables
| Variable | Required | Purpose |
|---|---|---|
GITLAB_AUDIT_LOG | No | Path to a JSON Lines audit log. When set, every request is audited (see Audit Logging). |
GITLAB_AUDIT_ACTOR | No | Overrides the recorded actor identity (for example, a CI service principal). |
GITLAB_DEBUG | No | Set to 1 to print a redacted traceback on failure. Never disables redaction. |
Audit Logging
When GITLAB_AUDIT_LOG is set, the script writes a structured JSON Lines audit trail for every REST API and OAuth form request. Auditing is fail-closed and write-ahead:
An
attemptrecord is written before the request is sent. If the audit log cannot be written, the operation is aborted and nothing is sent to GitLab.An
outcomerecord (successorerror, with HTTP status on failure) is written after the request completes.
REST records include a UTC timestamp, actor, auth mode, normalized origin, operation, HTTP method, and event. OAuth records use bounded operation and failure kinds without actor, origin, client, profile, code, body, or free-form error fields. Resource paths, queries, profile names, and profile contents are excluded. The optional audit sink is operationally sensitive because origins can identify tenants or internal hosts. Place it at an access-controlled path and retain it only as long as operations require. Tokens and authorization headers are never written. Audit failures after the request emit a warning without altering the result.
Credential Rotation
OAuth is the default. Register a non-confidential user-, group-, or self-managed
instance-owned application with redirect URI
http://127.0.0.1:8766/callback and the api scope. Set only
GITLAB_URL and GITLAB_OAUTH_CLIENT_ID, then run auth login or the
human-assisted auth device-login. Browser login requires a rotating refresh
token; access tokens refresh before expiry and replacements are stored
atomically. Device login can return an access-token-only profile and then
requires another device login after expiry. A provider/local crash window can
still require re-login.
The default store is $XDG_DATA_HOME/hve-core/gitlab/gitlab-token.json on
POSIX, falling back to ~/.local/share/hve-core/gitlab/gitlab-token.json.
POSIX store and lock files use mode 0600 inside a dedicated owner-only
directory. OAuth profile persistence fails closed on Windows because Python
3.11 mode bits cannot enforce an owner-only DACL. Use explicit
legacy-token mode on Windows until a protected OAuth backend is available.
PAT authentication remains available through GITLAB_AUTH_MODE=legacy-token.
Device authorization requires user approval on a browser-capable device and is
not unattended workload identity.
Quick Start
Export your environment variables, then run a read command with --fields.
export GITLAB_URL="https://gitlab.com"
export GITLAB_OAUTH_CLIENT_ID="your-application-id"
export GITLAB_PROJECT="group/project"
python scripts/gitlab.py auth login
python scripts/gitlab.py mr-list opened --fields iid,title,author.nameRead pipeline jobs for a known pipeline:
python scripts/gitlab.py pipeline-jobs 12345 --fields id,name,status,stageParameters Reference
Common Option
| Parameter | Applies To | Example | Description |
|---|---|---|---|
--fields | mr-list, mr-get, mr-notes, pipeline-get, pipeline-jobs | --fields iid,title,state | Extract specific fields with dot notation and print concise tabular or key-value output |
Commands
| Command | Arguments | Description |
|---|---|---|
auth login | None | Authenticate with Authorization Code and PKCE |
auth device-login | None | Authenticate through human-assisted device authorization |
auth status | None | Print secret-free local profile status |
auth logout | None | Delete one local profile without revoking server authorization |
mr-list | [state] [max] | List merge requests, defaulting to all states and 20 results |
mr-get | <mr-iid> | Get one merge request by project-scoped IID |
mr-create | <json> or stdin | Create a merge request from a JSON payload |
mr-update | <mr-iid> <json> or stdin | Update merge request fields from a JSON payload |
mr-comment | <mr-iid> <body> or stdin | Add a comment to a merge request |
mr-notes | <mr-iid> [max] | List merge request notes, excluding system notes when using --fields |
pipeline-get | <pipeline-id> | Get one pipeline by numeric ID |
pipeline-run | <branch-or-tag> | Trigger a pipeline for a branch or tag |
pipeline-jobs | <pipeline-id> | List jobs for a pipeline |
job-log | <job-id> | Print raw log output for a job |
Script Reference
List recent open merge requests:
python scripts/gitlab.py mr-list opened --fields iid,title,author.name,user_notes_countGet one merge request:
python scripts/gitlab.py mr-get 42 --fields iid,title,state,source_branch,target_branchCreate a merge request from inline JSON:
python scripts/gitlab.py mr-create '{
"source_branch": "feature/add-auth",
"target_branch": "main",
"title": "feat(auth): add OAuth login"
}'Add a merge request comment from standard input:
echo "CI passed. Ready for review." | python scripts/gitlab.py mr-comment 42Inspect a failed pipeline:
python scripts/gitlab.py pipeline-get 12345 --fields id,status,web_url
python scripts/gitlab.py pipeline-jobs 12345 --fields id,name,status,stage
python scripts/gitlab.py job-log 67890Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
GITLAB_URL is not set | Required environment variable missing | Export GITLAB_URL before running the script |
GITLAB_TOKEN is not set for legacy-token mode | Explicit legacy mode has no PAT | Export a PAT only with GITLAB_AUTH_MODE=legacy-token |
GITLAB_TOKEN must not be set in oauth mode | Legacy PAT leaked into the OAuth configuration | Remove GITLAB_TOKEN or explicitly select legacy-token mode |
| OAuth persistence is unavailable on Windows | No protected Windows profile-store backend | Use explicit legacy-token mode or run OAuth from POSIX |
GITLAB_OAUTH_CLIENT_ID is not set | OAuth application is not configured | Set the public client ID, then run auth login |
| Callback listener cannot bind | Port 8766 is unavailable or not forwarded | Free the port or use auth device-login |
timed out waiting for the GitLab OAuth token store lock | Another process holds the profile-store lock | Wait for the other command to finish, then retry |
GitLab device response has invalid verification_uri | The verification URI is unsafe to display or does not match GITLAB_URL | Confirm GITLAB_URL matches the instance that serves device verification |
cannot parse git remote URL | Project autodetection failed | Set GITLAB_PROJECT explicitly |
HTTP 401 or HTTP 403 | Credential is invalid or lacks access | Re-login for OAuth; rotate the PAT in explicit legacy mode |
HTTP 404 | Wrong project, MR IID, pipeline ID, or job ID | Verify GITLAB_PROJECT and confirm the numeric identifiers |
expected numeric ID | Non-numeric value passed to an ID argument | Use project MR IID values and numeric pipeline or job IDs |
python3 is required or syntax errors on launch | Unsupported interpreter | Run the script with Python 3.11 or later |
GitLab uses MR IIDs such as !42 inside a project. This skill expects the
numeric IID, not the global merge request ID.
Files
- ATTRIBUTION.mdAdded by AutoGineer825 B
- LICENSELicence file added by AutoGineer1.1 KiB
- SECURITY.md33.4 KiB
- SKILL.md12.9 KiB
- pyproject.toml801 B
- scripts/_gitlab_credentials.pyScript15.1 KiB
- scripts/_gitlab_oauth.pyScript24.0 KiB
- scripts/gitlab.pyScript53.5 KiB
- tests/conftest.pyScript5.8 KiB
- tests/corpus/0814fd5a169c01a1448b1817d8ac617d6f277db8(opens on GitHub)Not mirrored: opens on GitHub6 B
- tests/corpus/0_empty(opens on GitHub)Not mirrored: opens on GitHub1 B
- tests/corpus/0_large(opens on GitHub)Not mirrored: opens on GitHub2.0 KiB
- tests/corpus/0_no_suffix(opens on GitHub)Not mirrored: opens on GitHub5 B
- tests/corpus/0_remote_path(opens on GitHub)Not mirrored: opens on GitHub18 B
- tests/corpus/0_unicode(opens on GitHub)Not mirrored: opens on GitHub11 B
- tests/corpus/0_with_suffix(opens on GitHub)Not mirrored: opens on GitHub9 B
- tests/corpus/1_empty1 B
- tests/corpus/1_float5 B
- tests/corpus/1_large15 B
- tests/corpus/1_negative3 B
- tests/corpus/1_numeric_id6 B
- tests/corpus/1_valid_id6 B
- tests/corpus/2_deeply_nested604 B
- tests/corpus/2_empty1 B
- tests/corpus/2_field_path12 B
- tests/corpus/2_nested_json26 B
- tests/corpus/2_unicode29 B
- tests/corpus/3_empty1 B
- tests/corpus/3_json_payload15 B
- tests/corpus/3_large2.0 KiB
- tests/corpus/3_malformed10 B
- tests/corpus/3_trailing10 B
- tests/corpus/3_valid_json35 B
- tests/corpus/4_empty1 B
- tests/corpus/4_float5 B
- tests/corpus/4_large15 B
- tests/corpus/4_negative3 B
- tests/corpus/4_positive_int3 B
- tests/corpus/4_unicode10 B
- tests/corpus/4_zero2 B
- tests/corpus/5_empty1 B
- tests/corpus/5_field_flag18 B
- tests/corpus/5_large899 B
- tests/corpus/5_mr_list8 B
- tests/corpus/5_multi_field25 B
- tests/corpus/5_single_field12 B
- tests/corpus/5_unicode25 B
- tests/corpus/README.md1.5 KiB
- tests/corpus/ee123d57ee4d24cece23066fb721013d717824f2(opens on GitHub)Not mirrored: opens on GitHub37 B
- tests/fuzz_harness.pyScript7.6 KiB
- tests/test_constants.pyScript1.2 KiB
- tests/test_gitlab_audit.pyScript7.8 KiB
- tests/test_gitlab_commands.pyScript10.0 KiB
- tests/test_gitlab_coverage.pyScript4.2 KiB
- tests/test_gitlab_credentials.pyScript13.2 KiB
- tests/test_gitlab_helpers.pyScript25.7 KiB
- tests/test_gitlab_main.pyScript13.2 KiB
- tests/test_gitlab_oauth.pyScript18.1 KiB
- tests/test_gitlab_transport.pyScript31.1 KiB
- uv.lock57.5 KiB
SECURITY.md
title: GitLab Skill Security Model description: STRIDE threat model for the GitLab skill covering API egress, credentials, git project resolution, caller input, the OAuth callback listener, and profile storage author: microsoft/hve-core ms.date: 2026-08-18 ms.topic: reference estimated_reading_time: 11 keywords:
security
STRIDE
gitlab
rest cli
threat model
<!-- markdownlint-disable-file -->
GitLab Skill Security Model
This document records the STRIDE threat model for the GitLab skill (scripts/gitlab.py). The model is organized by trust bucket: CLI → GitLab API (B1), Credential configuration (B2), Git remote subprocess / project resolution (B3), CLI caller process (B4), Browser → fixed loopback callback (B5), and OAuth profile store and lock (B6). Each bucket enumerates all six STRIDE categories with the in-code mitigations that address them. Assets and adversaries are enumerated first. Acknowledged enterprise readiness gaps are listed at the end.
The skill is a standard-library-only CLI with provider-local OAuth and
credential modules. OAuth mode runs a bounded loopback listener for PKCE or a
human-assisted device flow and persists profile-bound tokens in a POSIX
mode-0600 store. OAuth persistence fails closed on Windows until a protected
backend exists. Explicit legacy mode reads a PAT from the environment. The CLI also
spawns one read-only git remote get-url origin subprocess when
GITLAB_PROJECT is unset.
See also: repo-wide STRIDE model. This skill participates in the repository-wide threat model at
docs/security/security-model.mdand is registered in its Skill Security Models section.
Executive Summary
The GitLab skill uses public-client OAuth with PKCE or human-assisted device
authorization by default. Short-lived access tokens refresh through rotating
refresh tokens stored in an instance/client-bound POSIX mode-0600 profile store.
Explicit legacy-token mode retains PAT support. All OAuth and API egress uses
TLS and the hardened no-redirect opener. Residual risks include the
provider/local refresh-commit crash window, same-user access to the token store,
and server-side authorization that local logout does not revoke.
Security Posture Overview
| Dimension | Value |
|---|---|
| Runtime surface | REST CLI (stdlib only); OAuth loopback/device flow; token store; read-only git subprocess |
| Trust buckets | B1 API, B2 config, B3 git remote, B4 caller, B5 callback, B6 profile store |
| Credentials | OAuth Bearer access/refresh tokens by default; explicit PAT legacy mode |
| Network egress | HTTPS to GitLab OAuth/API endpoints (no-redirect); fixed HTTP loopback callback |
| Open residual gaps | 8 (EoP-Med: skill cannot revoke a leaked token) |
Contents
System Description
Trust Boundaries
Assets
Adversaries
Bucket B1: CLI → GitLab API
Bucket B2: Credential profiles and environment
Bucket B3: Git remote subprocess / project resolution
Bucket B4: CLI caller process
Bucket B5: Browser → fixed loopback OAuth callback
Bucket B6: OAuth profile store and lock
Enterprise Readiness Gaps
References
System Description
Components
scripts/gitlab.py— CLI entry point, API transport, command dispatch, and audit logging.scripts/_gitlab_oauth.py— public-client PKCE, device authorization, token parsing, and refresh exchange.scripts/_gitlab_credentials.py— profile validation, POSIX mode-0600 persistence, Windows fail-close, and cross-process locking.Hardened opener (
_OPENER/_NoRedirect) — enforces TLS, refuses 30x redirects, and caps response bodies.git remote get-url originsubprocess — read-only project resolution whenGITLAB_PROJECTis unset.
Data Flow
flowchart TD
subgraph HOST["Operator Workstation / Runner (trust zone)"]
CLI["gitlab.py CLI"]
ENVCRED["URL / auth mode / public client ID<br/>or explicit legacy PAT"]
GIT["git remote get-url origin<br/>(argv, no shell)"]
LOOP["Fixed callback<br/>127.0.0.1:8766/callback"]
STORE["OAuth profile store + lock<br/>(0700 dir, 0600 files)"]
OUT["JSON / redacted job traces / audit log"]
end
subgraph BROWSER["User browser (authorization boundary)"]
CONSENT["GitLab consent page"]
end
subgraph GL["GitLab Instance (network boundary)"]
OAUTH["OAuth authorize / device / token endpoints"]
API["GitLab REST API + CI job traces"]
end
CLI -->|"reads per invocation"| ENVCRED
CLI -->|"resolve project on cache miss"| GIT
CLI -->|"open authorize URL + PKCE challenge"| CONSENT
CONSENT -->|"code + state over HTTP loopback"| LOOP
LOOP -->|"validated code + state"| CLI
CLI -->|"token exchange / device poll / refresh<br/>(HTTPS, no-redirect)"| OAUTH
OAUTH -->|"short-lived access + rotating refresh token"| CLI
CLI -->|"atomic profile commit"| STORE
STORE -->|"locked bound profile read"| CLI
CLI -->|"Bearer or explicit PRIVATE-TOKEN<br/>(HTTPS, no-redirect)"| API
API -->|"MR/pipeline payloads + CI trace (untrusted)"| CLI
CLI -->|"writes (redacted, truncated)"| OUTTrust Boundaries
Boundary Diagram
┌──────────────────────────────────────────────────┐
│ TRUST BOUNDARY: Operator Workstation / Runner │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐ │
│ │ gitlab │ │ Env config│ │ git remote│ │ output │ │
│ │ CLI │ │ / PAT │ │ subprocess│ │ │ │
│ └────┬─────┘ └───────────┘ └──────────┘ └─────────┘ │
│ ├── 127.0.0.1:8766 callback │
│ └── POSIX owner-only OAuth profile store + lock │
└────────────────────────┬──────────────────────────┘
open browser │ HTTPS (TLS, no-redirect)
┌───────────────┐ ┌────▼──────────────────────────────┐
│ User browser │ │ BOUNDARY: GitLab Instance │
│ consent page │ │ OAuth endpoints + REST API │
└───────────────┘ └───────────────────────────────────┘Boundary Descriptions
| Boundary | Assets Protected | Controls Enforced |
|---|---|---|
| Operator Workstation / Runner | PAT, output, local git config | Per-invocation env resolution; redaction; sanitized remote URL; argv (no shell) |
| User browser and callback | Code, state, PKCE verifier | Fixed loopback address/path; Host and state checks; bounded wait; PKCE |
| OAuth profile store and lock | Access/refresh tokens, binding | POSIX owner-only directory; 0600; O_NOFOLLOW; descriptor validation; lock; atomic replacement; Windows fail-close |
| GitLab Instance | Request/response integrity, token | TLS (system trust store); _NoRedirect; origin-only base URL; capped JSON parser; CI trace redacted/truncated |
Assets
| Id | Asset | Lifetime | Notes |
|---|---|---|---|
| A1 | GitLab OAuth access token | Short-lived | Stored in a bound profile and sent as Authorization: Bearer. |
| A2 | Rotating OAuth refresh token | Provider-managed | Exchanged only against the trusted issuer/client binding and stored in the owner-only profile store. |
| A3 | Explicit legacy PAT | Operator-managed | Read from GITLAB_TOKEN only when GITLAB_AUTH_MODE=legacy-token. |
| A4 | OAuth code, device code, state, verifier | One flow | Temporary capabilities; the PKCE verifier and state remain process-local. |
| A5 | Profile store and lock state | Persistent | Schema, issuer/client binding, usability, expiry, scopes, and cross-process coordination state. |
| A6 | GITLAB_URL and public client ID | Operator-managed | Trusted issuer/client identity used to bind profiles and egress. |
| A7 | Git remote URL | Command lifetime | May embed credentials; sanitized before diagnostics. |
| A8 | API payloads, CI traces, diagnostics | Command lifetime | Untrusted server content and optional operational audit data; never contains intentional credential output. |
Adversaries
| Id | Adversary | In-scope mitigations |
|---|---|---|
| ADV-a | Same-uid malware on the operator workstation | Not defended. A process running as the operator can read the environment and git config directly. Workstation hygiene is the controlling defense. |
| ADV-b | Network attacker on the CLI ↔ GitLab channel | TLS with stdlib certificate validation; HTTP redirects refused (_NoRedirect); HTTPS required for non-loopback hosts; capped, content-type-checked response parser. |
| ADV-c | Hostile or malformed GitLab server / response | No-redirect opener; response size cap (MAX_BODY_BYTES); JSON content-type fail-closed; error and non-JSON bodies redacted and size-previewed before display. |
| ADV-d | Hostile local git remote | Subprocess uses an arg list (no shell) with an explicit timeout; embedded credentials stripped for logging (_sanitize_remote_url); resolved path validated (_validate_project_path). |
| ADV-e | Hostile caller process controlling argv / stdin / env | Inputs validated/encoded; GITLAB_URL canonicalized to origin-only; state/ref/numeric IDs validated; stdin/JSON size-capped before parse. |
Bucket B1: CLI → GitLab API
All REST calls target the configured GITLAB_URL over urllib.request through a hardened opener.
Spoofing
TLS certificate validation is enforced by the stdlib default
SSLContext(system trust store).GITLAB_URLis reduced to an origin-only URL by_normalize_base_url, which rejects embedded userinfo so a crafted value cannot impersonate a host with inline credentials.
Tampering
TLS protects request and response bodies in transit.
The shared opener
_OPENERis built with_NoRedirect, which raises on any 30x so a redirect cannot silently retarget the request.Interpolated values are validated/encoded: the project path via
urllib.parse.quote(safe=""), merge-requeststateviavalidate_state, pipelinerefviavalidate_ref, and numeric IDs viavalidate_numeric_id/validate_positive_int(which reject0and enforce upper bounds).
Repudiation
Commands emit deterministic exit codes (
EXIT_SUCCESS/EXIT_FAILURE/EXIT_USAGE).The attempt audit record is write-ahead and fail-closed. The outcome record is best-effort; see Enterprise Readiness Gaps.
Information Disclosure
OAuth uses
Authorization: Bearer; explicit legacy mode usesPRIVATE-TOKEN. Both are sent only over TLS and are never logged.The token-bearing opener blocks 30x, preventing a hostile redirect from forwarding the token to a non-GitLab origin (
_NoRedirect).The transport requires a JSON content type when JSON is expected and reads through
_read_capped; a missing or non-JSON content type fails closed.HTTP error bodies are parsed first, then redacted for presentation; a dict error without
message/errorproduces a single structured redacted summary (_request_bytes). Non-JSON bodies that the CLI prints for usability are passed through_redactand capped via_preview_text._redactscrubs theBearer/Basicscheme-plus-value pair first, thenPRIVATE-TOKEN/X-API-Key/Authorization/Proxy-Authorization/Cookie/Set-Cookie/token/password/secretand query-string secrets. It additionally applies_REDACT_PATTERNS, which mask every_REDACT_KEYSentry in JSON shape ("key": "value"), bare form shape (key=value, with no leading?or&required), and Azure Blob SAS query strings.code_challengeis deliberately excluded because a PKCE challenge is public by design and masking it would corrupt an authorization URL.Transport failures raise
GitLabAPIError, whose__str__renders only the status, method, query-stripped resource, redacted summary, and a character-validated request ID. The raw response body, the query-bearing request URL, and the header dictionary are never part of the rendered string (_scrub_url,_response_request_id).The resolved credential lives only on the frozen
AuthContext, whosetokenfield carriesrepr=False. There is no module-level token global, sorepr(globals()), an accidentalrepr(auth_context), and traceback frame dumps cannot surface the PAT. A source-contract test asserts the global stays absent.
Denial of Service
Response bodies are read through
_read_cappedwith aMAX_BODY_BYTEScap.Requests use
REQUEST_TIMEOUTso a stalled server cannot hang the CLI.
Elevation of Privilege
The skill issues only the operations exposed by its explicit subcommands; request URLs are built only from validated, encoded segments.
TLS posture
Every GitLab call uses the stdlib opener with no custom SSLContext, CA-bundle flag, or pinning. Operators inherit Python's default HTTPS behavior: validation uses the system trust store; internal CAs require SSL_CERT_FILE/SSL_CERT_DIR; there is no pinning or mTLS (G-TLS-1). HTTPS is required for non-loopback hosts; plaintext http:// is refused for non-loopback hosts even when GITLAB_ALLOW_INSECURE=1 is set, and is permitted only for loopback hosts when GITLAB_ALLOW_INSECURE=1 is explicitly set — an opt-in gate mirroring the jira skill. cmd_job_log continues to emit redacted, untrusted CI trace content with truncation.
Risk Rating
| Threat | Likelihood | Impact | Residual Risk | Status |
|---|---|---|---|---|
| TLS MITM / hostile redirect retargeting | Low | High | Low | Mitigated (TLS + _NoRedirect) |
| Plaintext HTTP to a non-loopback host | Low | High | Low | Mitigated (refused) |
| Oversized-response memory exhaustion | Low | Low | Low | Mitigated (cap + timeout) |
Bucket B2: Credential profiles and environment
The instance origin and mode are read from the environment. OAuth mode loads an
instance/client-bound local profile containing short-lived access and rotating
refresh tokens. Legacy mode reads a PAT from GITLAB_TOKEN.
Spoofing
GITLAB_URLis parsed withurllib.parse.urlsplit, must usehttp/https, and HTTPS is enforced for non-loopback hosts (_is_loopback).
Tampering
_normalize_base_urlrejects control characters, embedded userinfo, query, fragment, and non-root paths, reducing the value to an origin-only URL before any request is built.
Repudiation
Missing, malformed, mixed, or mismatched credentials fail fast with a usage exit code via
die.
Information Disclosure
OAuth profiles use atomic mode-0600 writes in a dedicated POSIX owner-only directory and never appear in status output. Windows OAuth persistence fails closed. PAT values remain environment-only. Neither credential form is logged.
Denial of Service
Profile reads and writes are serialized with an advisory lock whose cross-process acquisition is bounded by a finite timeout.
Elevation of Privilege
The token's effective permissions are governed entirely by GitLab; the skill adds no privilege.
Risk Rating
| Threat | Likelihood | Impact | Residual Risk | Status |
|---|---|---|---|---|
| Base-URL host impersonation (embedded userinfo) | Low | Med | Low | Mitigated (origin-only) |
| OAuth refresh token at rest | Med | High | Med | Partially mitigated (0600 + binding) |
| PAT at rest in operator environment | Low | High | Med | Not defended (workstation hygiene) |
Bucket B3: Git remote subprocess / project resolution
When GITLAB_PROJECT is unset, the skill resolves the project from the local git remote (project()).
Spoofing
The remote is read from the local git configuration, which is operator-controlled; a hostile local remote is modeled as ADV-d and constrained by path validation below.
Tampering
The subprocess is invoked with an argument list (
["git", "remote", "get-url", "origin"]) andshellis never used, so remote values cannot inject shell commands.The resolved path is validated by
_validate_project_path, which rejects%, backslashes, and empty/./..segments before it is URL-encoded — blocking path traversal and encoded-separator escapes.
Repudiation
Resolution failures (
CalledProcessError,FileNotFoundError,TimeoutExpired) map to explicit exit codes with messages that reference only the sanitized remote URL.
Information Disclosure
_sanitize_remote_urlstrips embedded credentials from the remote URL before it appears in any error message, so a remote likehttps://user:pass@host/group/project.gitnever leaks its secret to stderr.
Denial of Service
subprocess.check_outputis bounded bytimeout=REQUEST_TIMEOUT; a stalled or blockedgitinvocation surfaces as a typed timeout error rather than hanging the CLI.
Elevation of Privilege
The resolved project path becomes an API path component, but it is validated and encoded;
GITLAB_PROJECTcan be set explicitly to bypass remote resolution entirely for privileged or destructive operations.
Risk Rating
| Threat | Likelihood | Impact | Residual Risk | Status |
|---|---|---|---|---|
| Shell injection via hostile remote URL | Low | High | Low | Mitigated (argv, no shell) |
| Path traversal via resolved project path | Low | Med | Low | Mitigated (_validate_project_path) |
| Credential leak from remote URL in logs | Low | High | Low | Mitigated (_sanitize_remote_url) |
Stalled git subprocess hang | Low | Low | Low | Mitigated (timeout) |
Bucket B4: CLI caller process
The caller controls argv, environment, stdin, stdout, and stderr; the CLI treats that process as operator-controlled.
Spoofing
API commands have no listener or attach surface. The bounded
auth loginlistener is modeled separately in B5.
Tampering
Arguments are parsed locally; handlers validate identifiers,
state,ref, and numeric IDs, and encode interpolated segments before issuing requests.JSON payloads are parsed through
load_json_payloadwith a size cap enforced beforejson.loads.
Repudiation
Validation, authentication, and runtime failures map to distinct exit codes for attribution by a calling step.
Information Disclosure
Command output is JSON-encoded GitLab payloads; tokens never appear in normal output.
Diagnostic and raw-text writes use
_emit,_emit_stdout, or_emit_debug_tracebackand apply_redact. Successful structured JSON and TSV are sanitized by sensitive key before_emit_structured_stdout, preserving syntax and field arity. Arbitrary credentials copied into non-sensitive successful free text remain a residual gap.Job traces are redacted via
_redactand truncated atMAX_LOG_BYTESbefore printing (cmd_job_log), so a token echoed into a CI trace is masked and an oversized trace is truncated rather than hard-failing.Credentialed traffic has two explicit owners: REST requests use
gitlab._request_bytes, and OAuth forms use_gitlab_oauth.post_form. A recursive production-module source contract rejects direct egress elsewhere, while behavioral tests prove write-ahead audit attempts and bounded outcomes for both owners.GITLAB_DEBUG=1enables a traceback on the failure path; the formatted traceback is redacted before it is written.LOGGER.exceptionis banned by a source-contract test because it would emit an unredacted traceback.GitLab-authored text returned in output must be treated as untrusted by downstream automation.
Denial of Service
HTTP bodies are read through
_read_capped; job logs are truncated atMAX_LOG_BYTES; stdin/JSON payloads are size-capped; pagination is bounded byvalidate_positive_int.
Elevation of Privilege
No command path bypasses input validation or constructs an unencoded request URL from caller input.
Risk Rating
| Threat | Likelihood | Impact | Residual Risk | Status |
|---|---|---|---|---|
| Token echoed into a CI job trace | Med | High | Low | Mitigated (_redact + truncate) |
| Untrusted GitLab / CI text consumed downstream | Med | Med | Med | By design (consumer responsibility) |
| Oversized stdin / job-log payload | Low | Low | Low | Mitigated (caps / truncation) |
| Leaked token not revocable by the skill | Low | High | Med | Accepted upstream (G-EOP-1) |
Bucket B5: Browser → fixed loopback OAuth callback
auth login binds 127.0.0.1:8766 before opening the authorization URL and accepts a callback only at /callback.
Spoofing
The handler validates the exact Host header and callback path, then compares UTF-8 state bytes with
secrets.compare_digestbefore accepting completion.PKCE S256 binds a captured authorization code to the verifier held by the CLI process.
Tampering
The listener binds only to IPv4 loopback, sets
allow_reuse_address = False, and rejects wrong hosts, paths, missing state, and incomplete callback parameters.
Repudiation
Authentication commands use distinct operation names and deterministic exit codes. Authorization URL output contains no secret verifier or token.
Information Disclosure
The callback response is minimal text and contains no token, authorization code, or profile content.
Token exchange uses the HTTPS no-redirect opener only after state validation.
Device verification instructions are validated before display. Values carrying Unicode control characters, and verification URIs with embedded credentials, raw spaces, backslashes, unparseable components, or an origin that differs from the configured issuer, are rejected instead of printed. The user code itself is preserved unchanged because its format belongs to the authorization server. This is local defense-in-depth and deliberately narrows support for deployments that publish device verification on a different host.
Denial of Service
Bind failure is converted to a typed error with a device-login recovery path. Invalid local requests do not terminate the serve loop, each accepted peer has a five-second read timeout, and one shared deadline bounds listener service and caller waiting.
Elevation of Privilege
The handler accepts only a valid code or provider error paired with the expected state. Authorization remains subject to the user-visible GitLab consent page and registered application scopes.
Risk Rating
| Threat | Likelihood | Impact | Residual Risk | Status |
|---|---|---|---|---|
| Forged callback or code interception | Low | High | Low | Mitigated (state + PKCE) |
| Local callback-port denial | Low | Low | Low | Mitigated (typed device flow) |
| Callback wait exhaustion | Low | Low | Low | Mitigated (shared deadline) |
| Hostile device-instruction output | Low | Med | Low | Mitigated (validated before display) |
Bucket B6: OAuth profile store and lock
On POSIX, OAuth profiles are stored beneath a dedicated owner-only directory with a sibling advisory lock. The store contains access and refresh tokens plus issuer/client binding and usability state. Windows persistence fails closed before filesystem access.
Spoofing
Every stored issuer is validated as an origin-only HTTPS URL (or explicit loopback HTTP), and each locked reread must match the already trusted issuer and client ID before refresh or API use.
Tampering
Store and lock opens use
O_NOFOLLOW; opened descriptors must be regular files owned by the effective user without group or world permissions.Writes validate the schema, use mode
0600, fsync file content, atomically replace the store, and fsync the parent directory. Cross-process updates require an advisory lock.
Repudiation
Profiles record obtained and expiry timestamps, scopes, issuer, client ID, and usability. The store is not a signed audit record and must not be treated as one.
Information Disclosure
The dedicated POSIX directory is owner-only and store/lock files use mode
0600. Status output excludes access and refresh tokens.Same-uid malware remains outside the defended boundary (G-INF-3).
Denial of Service
Missing locking primitives fail closed. Corrupt schemas, unsafe permissions, and symlinks fail before credential use. Windows OAuth persistence is unavailable until a protected backend exists.
Cross-process lock acquisition is non-blocking with a finite deadline, so a stalled lock holder surfaces a typed timeout instead of hanging the CLI. Only lock-contention errors are retried; other locking failures propagate unchanged. The bound covers acquisition, not in-process waiting or work performed while the lock is held.
A provider/local refresh-commit uncertainty can require re-login (G-DOS-1).
Elevation of Privilege
Profiles cannot change the trusted issuer/client binding during a locked refresh. GitLab remains the authority for granted scopes; local storage cannot broaden them.
Risk Rating
| Threat | Likelihood | Impact | Residual Risk | Status |
|---|---|---|---|---|
| Cross-user POSIX store disclosure | Low | High | Low | Mitigated (owner-only directory and files) |
| Unsupported Windows persistence | Low | High | Low | Mitigated (fails closed before store access) |
| Symlink or replacement tampering | Low | High | Low | Mitigated (no-follow + descriptor checks) |
| Same-uid credential access | Med | High | Med | Accepted boundary (G-INF-3) |
| Refresh commit uncertainty | Low | Med | Med | Fail closed; re-login (G-DOS-1) |
Enterprise Readiness Gaps
The following are known limitations recorded so operators can make informed deployment decisions. Severity ratings are the project's own assessment and are not equivalent to a CVSS score.
| Id | Gap | Severity | Status |
|---|---|---|---|
| G-REP-1 | The write-ahead attempt is fail-closed, but the outcome is best-effort and the operator-supplied file is not signed or append-only. | Repudiation-Med | By design; integrate with host telemetry for tamper-evident logging. |
| G-INF-1 | The CLI prints redacted non-JSON response bodies to stdout/stderr for usability; redaction is a broad regex backstop that may over- or under-cover novel secret formats. | InfoDisc-Low | Accepted; monitor and extend _redact as new secret formats appear. |
| G-INF-2 | _REDACT_KEYS enumerates known credential field names; a novel or vendor-specific field name is masked only if it also matches the header, Bearer/Basic, or query-string rules. | InfoDisc-Low | Accepted; extend _REDACT_KEYS as new credential shapes appear. tests/test_gitlab_helpers.py pins the set. |
| G-INF-3 | Same-uid processes can read the OAuth store despite owner-only filesystem permissions. | InfoDisc-Med | Accepted workstation boundary; evaluate an OS keyring separately where policy requires it. |
| G-DOS-1 | A crash or malformed successful response between provider refresh rotation and durable local commit can invalidate the retained refresh token and require re-login. | DoS-Med | Fail closed by marking uncertain profiles unusable; re-run login. |
| G-EOP-1 | Local logout cannot revoke OAuth authorization, and the skill cannot revoke a leaked OAuth access token, refresh token, or legacy PAT. | EoP-Med | Upstream control; rotate or revoke at the GitLab instance on suspicion of compromise. |
| G-SUP-1 | Python dependencies are declared in pyproject.toml but transitive hashes are not pinned and no SBOM is published; transport/subprocess/redaction fuzz coverage is partial. | SupplyChain-Med | Tracked at the repository level. |
| G-TLS-1 | No certificate pinning for the GitLab origin; TLS validation depends entirely on the system trust store. | InfoDisc-Low | Operator-acceptable for a managed GitLab endpoint; documented for customers whose policy mandates pinning. |
For an active issue tracker entry covering these gaps, see microsoft/hve-core#2225.
References
Repository security model
🤖 Crafted with precision by ✨Copilot following brilliant human instruction, then carefully refined by our team of discerning human reviewers.
Requirements and compatibility
Written for: GitHub Copilot
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs an MCP server, Needs a command-line tool, Needs Python
The author says: Requires Python 3.11+. GitLab OAuth public-client credentials or explicit legacy-token mode.
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Microsoft
- Publisher
- Microsoft (Official)
- Source
- microsoft/hve-core · .github/skills/project-planning/gitlab · commit f425e94
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Microsoft.
Get it from the source
8 of this skill’s files are not mirrored here, so there is no ZIP. Get the complete skill from its source: microsoft/hve-core