Use this guide for pipeline-focused CLI usage across platforms.
The shell commands in the recommended patterns are CI-provider neutral. Buildkite pipeline equivalents and provider-specific considerations are called out alongside the relevant guidance below.
socketcli \
--reach \
--sarif-file results.sarif \
--sarif-scope full \
--sarif-grouping alert \
--sarif-reachability reachable \
--disable-blockingsocketcli \
--reach \
--sarif-file results.sarif \
--sarif-scope diff \
--sarif-reachability reachable \
--strict-blockingEither recommended pattern can run directly in a Buildkite command step. When the
scan writes SARIF, add
artifact_paths
so developers can download the report from the build after the command finishes:
steps:
- label: ":socket: Socket reachable diff"
command: |
socketcli \
--reach \
--sarif-file results.sarif \
--sarif-scope diff \
--sarif-reachability reachable \
--strict-blocking
artifact_paths:
- "results.sarif"Use --config .socketcli.toml or --config .socketcli.json to keep pipeline commands small.
Precedence order:
CLI flags > environment variables > config file > built-in defaults
Example:
[socketcli]
reach = true
sarif_scope = "full"
sarif_grouping = "alert"
sarif_reachability = "reachable"
sarif_file = "results.sarif"Equivalent JSON:
{
"socketcli": {
"reach": true,
"sarif_scope": "full",
"sarif_grouping": "alert",
"sarif_reachability": "reachable",
"sarif_file": "results.sarif"
}
}The Buildkite examples below use the same checked-in .socketcli.toml file; no
Buildkite-specific config-file format is required.
- name: Run Socket CLI
run: socketcli --config .socketcli.toml --target-path .
env:
SOCKET_SECURITY_API_TOKEN: ${{ secrets.SOCKET_SECURITY_API_TOKEN }}GitHub Actions paths filters only decide whether a workflow starts. They do not
change socketcli discovery or upload scope. For a merge gate, it is usually safer
to start a small selector job on every PR update, then create one scan job per
affected logical workspace. This also avoids a required check remaining pending
when GitHub skips the entire workflow because of a top-level path filter.
This pattern produces one dashboard entry per logical workspace, which is what gives each component its own alerts, baseline, and policy. It is also the layout that grows the dashboard's repository list. See Choosing a scan layout for when that trade-off is worth making.
Define a repository variable named SOCKET_MONOREPO_WORKSPACES_JSON. Its value is
an array with one stable workspace name, one or more scan roots, and the path globs
that should select that workspace. Fill these placeholders with the repository's
real layout. A workspace definition selects directory roots; shared root manifests,
lockfiles, and cross-directory path dependencies outside those roots are not included
automatically.
[
{
"name": "<stable-workspace-name>",
"sub_paths": ["<repo-relative-scan-root>"],
"watch_globs": ["<repo-relative-changed-file-glob>"]
}
]Each sub_paths value must be a directory, not an individual manifest or lockfile.
Using . includes the entire target path. Do not use this changed-workspace pattern
until the directory boundaries preserve every shared input needed to resolve each
logical graph. If root workspace metadata governs most or all of the repository, a
smaller coverage-preserving split may not be representable with --sub-path alone.
Also define SOCKETCLI_VERSION as the exact package version validated for the
workflow. The workflow below logs that version, uses full Git history for reliable
base/head selection, creates one matrix job (and therefore one graph and baseline)
per selected workspace, and fails closed on CLI/API/timeout failures. It uses API
SCM mode plus --enable-diff because parallel --scm github jobs can race while
updating the same PR comments; the matrix checks and report links are the gate.
name: Socket Security
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main]
permissions:
contents: read
jobs:
select-workspaces:
runs-on: ubuntu-latest
outputs:
count: ${{ steps.select.outputs.count }}
matrix: ${{ steps.select.outputs.matrix }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- id: select
name: Select changed workspaces
env:
WORKSPACES_JSON: ${{ vars.SOCKET_MONOREPO_WORKSPACES_JSON }}
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
shell: bash
run: |
python - <<'PY'
import fnmatch
import json
import os
import re
import subprocess
workspaces = json.loads(os.environ["WORKSPACES_JSON"])
if not isinstance(workspaces, list):
raise SystemExit("SOCKET_MONOREPO_WORKSPACES_JSON must be a JSON array")
base = os.environ["BASE_SHA"]
head = os.environ["HEAD_SHA"]
if not base or set(base) == {"0"}:
base = subprocess.check_output(
["git", "rev-parse", f"{head}^"], text=True
).strip()
changed_output = subprocess.check_output(
["git", "diff", "--name-only", "-z", base, head]
)
changed = [
item.decode("utf-8", "surrogateescape")
for item in changed_output.split(b"\0")
if item
]
selected = []
for workspace in workspaces:
name = workspace.get("name", "")
sub_paths = workspace.get("sub_paths") or []
watch_globs = workspace.get("watch_globs") or []
if not re.fullmatch(r"[A-Za-z0-9._-]+", name):
raise SystemExit(f"Invalid workspace name: {name!r}")
if not sub_paths or any(
not isinstance(path, str)
or path.startswith("/")
or ".." in path.split("/")
for path in sub_paths
):
raise SystemExit(f"Invalid sub_paths for workspace {name!r}")
if not watch_globs:
watch_globs = [
pattern
for path in sub_paths
for pattern in (
["*"]
if path.strip("/") in ("", ".")
else [path.rstrip("/"), f"{path.rstrip('/')}/*"]
)
]
if any(
fnmatch.fnmatchcase(path, pattern)
for path in changed
for pattern in watch_globs
):
selected.append({"name": name, "sub_paths": sub_paths})
matrix = json.dumps({"include": selected}, separators=(",", ":"))
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write(f"count={len(selected)}\n")
output.write(f"matrix={matrix}\n")
PY
scan-workspace:
needs: select-workspaces
if: needs.select-workspaces.outputs.count != '0'
timeout-minutes: 20
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.select-workspaces.outputs.matrix) }}
name: Socket scan (${{ matrix.name }})
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install pinned Socket CLI
env:
SOCKETCLI_VERSION: ${{ vars.SOCKETCLI_VERSION }}
run: |
python -m pip install "socketsecurity==$SOCKETCLI_VERSION"
socketcli --version
- name: Scan workspace
env:
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_SECURITY_API_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number || 0 }}
WORKSPACE_NAME: ${{ matrix.name }}
SUB_PATHS_JSON: ${{ toJSON(matrix.sub_paths) }}
shell: bash
run: |
set +e
args=(
--target-path "$GITHUB_WORKSPACE"
--workspace-name "$WORKSPACE_NAME"
--enable-diff
--pr-number "$PR_NUMBER"
--exit-code-on-api-error 3
--report-link-file socket-report-link.txt
--summary-file socket-summary.txt
)
while IFS= read -r sub_path; do
args+=(--sub-path "$sub_path")
done < <(jq -r '.[]' <<<"$SUB_PATHS_JSON")
socketcli "${args[@]}" 2>&1 | tee socket-output.log
code=${PIPESTATUS[0]}
{
echo "## Socket scan: $WORKSPACE_NAME"
if [ -s socket-report-link.txt ]; then
echo "[View the report]($(cat socket-report-link.txt))"
fi
if [ -s socket-summary.txt ]; then
echo '```'
cat socket-summary.txt
echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"
exit "$code"
socket-security:
if: always()
needs: [select-workspaces, scan-workspace]
runs-on: ubuntu-latest
steps:
- name: Enforce matrix result
env:
SELECT_RESULT: ${{ needs.select-workspaces.result }}
SCAN_RESULT: ${{ needs.scan-workspace.result }}
run: |
test "$SELECT_RESULT" = success
[[ "$SCAN_RESULT" = success || "$SCAN_RESULT" = skipped ]]Each configuration object may intentionally contain several sub_paths when
those directories are one logical dependency graph. To split backend resolution,
use separate objects with different name values. Add --workspace <name> only
when the Socket organization requires API workspace association; it is not a scan
scope control. Use --save-submitted-files-list in a non-required canary to verify
the exact manifests selected before adopting workspace-level scans as a merge gate.
The job has an explicit 20-minute total budget. Tune that value from observed
workspace-level latency after the split; a five-minute cap can still be too close
to a slow request plus local startup. The CLI's --timeout is different: it
defaults to 1,200 seconds per API request. If an operator adds GNU timeout,
that process supervisor can terminate the CLI before it maps an error through
--exit-code-on-api-error; without --preserve-status, GNU reports 124 after its
initial timeout signal or 137 if SIGKILL is involved.
This example assumes a GitHub-hosted repository. Change
SOCKET_SCM_INTEGRATION to gitlab for a GitLab-hosted repository, or api
when provider association is not wanted. The doubled dollar signs defer
Buildkite variable expansion until the command runs on an agent.
env:
SOCKET_SCM_INTEGRATION: "github"
steps:
- label: "Socket scan"
command: |
socketcli \
--config .socketcli.toml \
--target-path . \
--integration "$${SOCKET_SCM_INTEGRATION:-api}" \
--pr-number "$${BUILDKITE_PULL_REQUEST:-0}"
secrets:
- SOCKET_SECURITY_API_TOKENThe secrets block expects a
Buildkite secret
named SOCKET_SECURITY_API_TOKEN. If your organization uses an external secrets
plugin or an agent hook instead, remove that block and inject the same environment
variable through your existing mechanism. Do not store the token in pipeline YAML.
The CLI reads Buildkite's native BUILDKITE_COMMIT, BUILDKITE_BRANCH,
BUILDKITE_PULL_REQUEST, and BUILDKITE_PULL_REQUEST_BASE_BRANCH variables.
For pull-request builds, ensure the checkout contains the base branch and the
checked-out head commit. The CLI uses those local refs first and performs a
targeted fetch only when a required ref or its comparison history is missing;
it does not fetch every remote ref and tag during startup.
When --scm github is used from Buildkite, the CLI also posts GitHub PR comments.
It identifies the repository from BUILDKITE_REPO and takes the rest of the build
context from BUILDKITE_BUILD_CHECKOUT_PATH and the variables above — see
Buildkite PR context. Set GH_API_TOKEN to a GitHub token
with the required repository access. GitHub Enterprise users should also set
GITHUB_API_URL; GitHub.com defaults to https://api.github.com.
Notes for using --base-commit-sha (see the
merge-base note in the CLI reference)
when your steps are emitted by a
dynamic pipeline
generator rather than a static YAML file:
-
Compute the merge base at generation time, not step time. The generator runs with a full checkout; step agents may have shallow or fresh clones where
git merge-basefails or needs an extra fetch. Resolve it once in the generator and bake it into the emitted step'senv. Diff against the PR's target branch, which isn't always the default branch (see Buildkite's environment variables):TARGET="${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-$BUILDKITE_PIPELINE_DEFAULT_BRANCH}" BASE_SHA=$(git merge-base "origin/${TARGET}" HEAD)
-
If an exact baseline is required, emit a backfill step conditionally from the generator. The generator is the natural place for the "does an exact baseline scan exist?" check (
GET /orgs/{org}/full-scans?repo=<repo>&commit_hash=$BASE_SHA&per_page=1): only emit the baseline-scan step when it returns nothing. The emitted pipeline then shows in the UI whether a backfill will run. Without a backfill, the CLI automatically uses the nearest scanned first-parent ancestor within 100 commits and warns that the diff is wider. -
Keep the backfill inside one command step. The checkout-base → scan → checkout-PR sequence must not be split across steps — steps can land on different agents with different checkouts. Prefer
git worktreeover mutating the step's checkout:git worktree add /tmp/socket-base "$BASE_SHA"thensocketcli --target-path /tmp/socket-base --branch "$TARGET" --disable-blocking. -
Soft-fail infra errors, not findings. No reachable scanned ancestor (or any API error) exits with code 3 (
--exit-code-on-api-errorto change it); real findings exit 1.soft_fail: [{exit_status: 3}]on the PR scan step keeps infra errors from blocking merges while security findings still do. -
"Cancel intermediate builds" on the default branch is a common source of exact-baseline gaps. Canceled builds never scan their commit, so these PRs fall back to an older scanned ancestor. Use the conditional backfill step above when an exact merge-base comparison is required; there is no per-step exemption from build cancellation in Buildkite. If you need strict scan-once semantics for concurrent backfills of the same merge base, serialize the backfill step with a concurrency group keyed on the merge-base SHA.
socket_scan:
script:
- socketcli --config .socketcli.toml --target-path .
variables:
SOCKET_SECURITY_API_TOKEN: $SOCKET_SECURITY_API_TOKEN- script: |
socketcli \
--integration azure \
--enable-diff \
--target-path "$(Build.SourcesDirectory)"
env:
SOCKET_SECURITY_API_TOKEN: $(SOCKET_SECURITY_API_TOKEN)pipelines:
default:
- step:
script:
- socketcli --config .socketcli.toml --target-path .With --scm github or --scm gitlab, the detected event decides the scan type:
| Event | Scan | Blocks the build |
|---|---|---|
| Pull request / merge request | Diff scan against the repository's baseline | Yes, on newly introduced alerts |
| Any other pipeline, including default-branch pushes | Full scan | No |
A full scan has no baseline, so it cannot tell a newly introduced alert from one
that was already there. Rather than block on a number that would mean something
different depending on which output format was enabled, those runs behave as if
--disable-blocking was supplied and report through the Dashboard instead. This
matches how the CLI already treats a run with no supported manifest files.
The event type is authoritative once --scm is set: --enable-diff and
--ignore-commit-files do not turn a branch pipeline into a comparison. To diff
a branch build, drop --scm and use --enable-diff with --integration, which
runs the comparison without the PR comment adapter.
--generate-license and --legal-format fossa work on both paths; a full scan
fetches the package list for them.
The CLI sends the resolved pull request number with each full scan and attaches
the pull request URL to diff scans so the Socket Dashboard can associate the
report with its originating change. If --pr-number is supplied, it wins;
passing --pr-number 0 explicitly disables automatic association. Any value that
is not a positive integer, including Buildkite's false, means no pull request.
Without an explicit value, the CLI recognizes:
- GitHub Actions:
PR_NUMBER, then the PR number inGITHUB_REF. - GitLab CI:
CI_MERGE_REQUEST_IID. - Azure Pipelines:
SYSTEM_PULLREQUEST_PULLREQUESTNUMBERfor GitHub-hosted repositories, otherwiseSYSTEM_PULLREQUEST_PULLREQUESTIDfor Azure Repos.
Buildkite is SCM-provider neutral, so the CLI does not infer a provider or consume
its PR variable automatically. Pass Buildkite's
BUILDKITE_PULL_REQUEST
value to
--pr-number and identify the repository host with --integration, as shown in
the Buildkite platform example above. Buildkite sets BUILDKITE_PULL_REQUEST to
false outside PR builds; the CLI treats that value as no PR.
Use --integration github for GitHub-hosted repositories and --integration gitlab
for GitLab-hosted ones. The CLI identifies the repository from
BUILDKITE_REPO,
taking both the slug and the host from it, so github.com, GitLab.com, and self-hosted
installations all build a correct pull request or merge request link without extra
configuration. That same value identifies the repository for GitHub PR comments when
--scm github is set. CI_PROJECT_URL still overrides the derived GitLab project URL.
Keep --scm api unless you also intend to configure an existing GitHub or GitLab
comment adapter and its provider token.
--scm github and --scm gitlab also imply the matching scan integration for
Dashboard metadata unless --integration was explicitly supplied. PR comments
remain limited to the existing GitHub and GitLab SCM adapters; Azure receives
console output and Dashboard association but does not post a PR comment.
Prebuilt examples in this repo:
../workflows/github-actions.yml../workflows/buildkite.yml../workflows/gitlab-ci.yml../workflows/bitbucket-pipelines.yml
--strict-blockingenables strict diff behavior (new + unchanged) for blocking evaluation and diff-based output selection.--sarif-scope fullrequires--reach.--sarif-grouping alertcurrently applies to--sarif-scope full.- Diff-based SARIF can validly be empty when there are no matching net-new alerts.
- Keep API tokens in secret stores (
SOCKET_SECURITY_API_TOKEN), not in config files. - In Buildkite pipeline YAML, follow its
runtime interpolation
guidance and use
$$for variables that must expand when the command runs rather than when the pipeline is uploaded. - Security findings with
props.firstPatchedVersionIdentifiershow that value in the console table, including native Buildkite job logs, and in GitHub/GitLab security comments when that SCM adapter is configured. Findings without a known patched release leave the console cell blank and omit the comment field.