Install scripts

Check G16 · execution surface family · severity MEDIUM · automatic · one of the 22 GitLab checks

The question this check asks: does this gitlab.com project carry a script that the package manager will run, as you, the moment you install it — before you have read a line of what it does? It is C16, the GitHub check, run unchanged on a project hosted at gitlab.com.

Why it matters

Installing is running, and the host the files came from does not change that. Python's pip install . executes setup.py with your privileges by design — PEP 517 makes that the build interface, not a bug — and Python has no switch to turn it off. The JavaScript ecosystem spent a decade learning the same lesson through postinstall hooks until npm v12, in June 2026, turned lifecycle scripts off by default; pnpm 10 and later, Bun and Yarn PnP had already blocked them. The reason those defaults changed is that an install script is the cheapest place in a project to put a payload: it runs once, on every machine that installs, and nobody reads it because it is boilerplate. The one loader pattern GitLab's own threat-intelligence team has documented on gitlab.com — 131 accounts banned in 2025 — chose a neighbouring moment: a trigger invoked globally in a file that executes as soon as the project is run, with the payload fetched from elsewhere in more than 80% of cases.

The repository vetting standard's rule is short: never install with lifecycle scripts enabled; prefer wheels or a container. This check exists to tell you, before the clone, that the rule applies here.

How RepoGates scores it

G16 is C16 on a gitlab.com project: the same code, run on a snapshot RepoGates builds from gitlab.com's public API. The file list is one call — the recursive repository tree at per_page=10000, no clone, no file contents; inkscape/inkscape's 7,182 entries arrive in 0.36 s. A full page is reported as truncated rather than read as complete, and a project with more than one page — twelve of the 100 most-starred, gitlab-org/gitlab among them at 136,997 entries — is a partial scan: the tree checks ran on the first page only, the verdict is cached briefly and retried, and it is never a clean PASS. On that list the check fires when setup.py sits at the root. The finding is MEDIUM, costs 5 points, and reads “arbitrary python on pip install .”. A project that trips nothing else scores 95, the badge stays green, and the verdict is REVIEW; the default personal policy warns. It never blocks on its own. It belongs to the execution-surface family, whose evidence note in the scoring table explains the light weight for the whole group:

“28.8% of top-100 repos carry devcontainers — informative, not damning”

That note is GitHub's Phase 0 measurement. On 19 September 2026 the same code ran, anonymously, over the 100 most-starred gitlab.com projects: a root setup.py on 3 of them. Execution-surface findings are things legitimate projects do all the time. They are scored to be seen, not to be feared; the default personal policy warns on every one of them and blocks only on the two with a near-zero measured false-positive cost — a nested bare repository and hidden Unicode in an agent rules file.

Where the verdict reaches you today: the API (GET /v1/gl/score/{path}), the MCP server (check_repo with platform="gl"), and the Claude Code plugin's preflight (a hook on Claude Code's Bash tool: it reads the command line, never a process), which turns a warning into an ask before a git clone https://gitlab.com/… or a pip install git+https://gitlab.com/… line runs. The extension's board on gitlab.com pages is a later release.

False positives we know about

Most of the Python ecosystem written before 2022 fires this check, and gitlab.com hosts its share of it. A setup.py that contains nothing but a package name and a version looks, from the file list, identical to one that downloads a payload — the check sees the file, not what is in it — so it cannot be more than a warning. What the finding does is name the file, so that when you do install you do it the way the standard says: wheels only, scripts off, or inside a container. A deep scan reads the file itself and replaces the verdict with what it found.

What this check does not cover

It reads the file list, so it sees only what is named there. A package.json with a postinstall hook, a Makefile, an install.sh the README tells you to run — none of those fire G16, because telling them apart from harmless files of the same name needs their contents. Nor does the check see anything that runs only because you ran it — which is exactly where the documented gitlab.com loader sat: it fired when the project was run, after any install, and no install-script check would have named it. The related checks below cover the files that run without you: a devcontainer or a Workspaces devfile on open, an editor task on workspace trust, a Rust build script when the language server starts.

And RepoGates as a whole is a browser download gate and an answer an agent can ask for: it does not see git clone, package managers or curl — outside Claude Code with the RepoGates plugin, whose hook refuses a clone or install that names a blocked repository on the command line, before it runs. Even there it reads the line, never the script: the file that pip will run is named on the verdict, not opened.

RepoGates runs the same 22 checks on a gitlab.com project that it runs on a GitHub repository, from gitlab.com's public API, under the key gl/{namespace…}/{project}. What that API withholds from an anonymous caller — fork status, a user namespace's account age, and OpenSSF Scorecard, which does not cover gitlab.com — is listed as not checked, never assumed. A self-managed GitLab is another hostname and is not assessed. The repository's code is not read on this tier: a PASS means the provenance and the declared execution surface are clean.

Check it yourself

Without installing anything: look at the root of the project on gitlab.com for setup.py, package.json (and open it for a scripts block with preinstall or postinstall), Makefile, or any install.sh. With curl against gitlab.com's API — one call for the whole tree, the project path URL-encoded so / becomes %2F:

curl -s 'https://gitlab.com/api/v4/projects/NAMESPACE%2FPROJECT/repository/tree?recursive=true&per_page=10000' | jq -r '.[].path' | grep -E '^(setup\.py|package\.json|Makefile|install\.sh)$'

Then install the way the standard says, whatever you found:

pip install --only-binary=:all: PACKAGE
npm install --ignore-scripts

If a project cannot be installed without its script, install it in a container, and read the script first — it is usually short.

Questions

Does every Python project fire this check? Many older ones do — setup.py was the standard for years, and most of them are harmless; three of the hundred most-starred gitlab.com projects carry one. That is why the check is a warning worth 5 points, never a block: the finding tells you the file is there so you install with the scripts disabled or in a container, and the decision is yours. Projects built on pyproject.toml alone do not fire it.

Does a warning hold anything on gitlab.com today? The command line, not the download. In Claude Code with the RepoGates plugin, a git clone https://gitlab.com/… or a pip install git+https://gitlab.com/… that names the project becomes an ask, with the finding shown — here, that setup.py runs arbitrary Python on pip install — and proceeding is your call. The browser gate for gitlab.com's own download links is a later release. A block is reserved for findings with a measured near-zero false-positive cost, and this is not one of them.

Does RepoGates stop git clone of a gitlab.com project? In the browser, no: an extension cannot see a process outside it, and RepoGates does not see git clone, package managers or curl — outside Claude Code with the RepoGates plugin, whose hook refuses a clone or install that names a blocked repository on the command line, before it runs. This finding warns rather than blocks, so a clone of a project that carries it becomes an ask there, not a refusal. Nothing reads the script's contents on this tier; a deep scan does.

Related checks

C16 Install scripts — the GitHub original · G10 Devcontainer and Workspace devfile auto-execution — a command that runs when the folder opens, or when a GitLab Workspace is created; one of the two execution-surface findings graded critical · G14 Build scripts and wrapper JARsbuild.rs, Gradle and Maven wrappers, CMake · G17 Directory-entry hooks — an .envrc that runs on cd.

Add RepoGates to Chrome Pricing

Numbers on this page: the 100 most-starred gitlab.com projects, measured 19 September 2026 through the same code as the verdict API; GitLab's threat-intelligence post of 19 February 2026; npm v12's default change, June 2026; weights from the product's scoring table. Check numbering follows the product's catalogue.