Developer-environment provider
Scans the config files that run code the moment a developer opens or checks out the repository, a surface distinct from the CI pipeline definitions the rest of the scanner covers:
.vscode/tasks.jsontasks set torunOptions.runOn: folderOpen.devcontainer/devcontainer.jsonlifecycle commands (postCreateCommandand friends) and the host-sideinitializeCommand.claude/settings.jsonClaude Code hooks oftype: command
Text-only JSON(C) parsing (comments and trailing commas are
tolerated), no tokens, no network. The threat is the second stage of
campaigns like the 2026 Red Hat npm compromise: a poisoned repo that
runs a loader on folder-open / devcontainer-create / agent-session-
start, before any build or test. DEV-004 reserves CRITICAL for the
remote-fetch-and-execute shape.
Producer workflow
# Auto-detected when .vscode/ , .devcontainer/ , or .claude/ config
# files are present at cwd; defaults to scanning the current directory.
pipeline_check --pipeline devenv
# …or point it at a repo root or a single config file.
pipeline_check --pipeline devenv --devenv-path ./checkout
All other flags (--output, --severity-threshold, --checks,
--standard, …) behave the same as with the other providers.
What it covers
8 checks · 0 have an autofix patch (--fix).
| Check | Title | Severity | Fix |
|---|---|---|---|
| DEV-001 | VS Code task runs automatically on folder open | LOW | |
| DEV-002 | Devcontainer lifecycle command runs automatically | LOW | |
| DEV-003 | Committed Claude Code hook runs a shell command | MEDIUM | |
| DEV-004 | Auto-run command fetches and executes remote code | CRITICAL | |
| DEV-005 | Devcontainer initializeCommand runs unsandboxed on the host | HIGH | |
| DEV-006 | VS Code settings point a tool at a repo-local binary | HIGH | |
| DEV-007 | Committed MCP config auto-launches a local command server | MEDIUM | |
| DEV-008 | Credential-shaped literal in a developer-environment config | CRITICAL |
DEV-001: VS Code task runs automatically on folder open
Fires on any task in .vscode/tasks.json whose runOptions.runOn is folderOpen. VS Code Workspace Trust gates the first run, but reviewers routinely trust repos they open, so this is a real reachable-on-open surface rather than a purely theoretical one.
Recommended action
Remove runOptions.runOn: folderOpen so the task runs only when invoked explicitly, or move the logic into a documented setup script a developer chooses to run. If an auto-task is genuinely required, keep its command vendored in the repo and free of any network fetch (see DEV-004).
DEV-002: Devcontainer lifecycle command runs automatically
Fires when devcontainer.json declares any of onCreateCommand / updateContentCommand / postCreateCommand / postStartCommand / postAttachCommand. The host-side initializeCommand is handled separately by DEV-005 (it runs unsandboxed on the host).
Recommended action
Treat the lifecycle commands as code that runs on every Codespace / devcontainer create. Keep them vendored in the repo, free of network fetches (DEV-004), and review changes to them the way you would any executable in the build path. There is no way to disable lifecycle execution short of removing the keys; this finding is informational so a reviewer notices what runs on open.
DEV-003: Committed Claude Code hook runs a shell command
Fires on any hooks.<Event> entry of type: command in .claude/settings.json or .claude/settings.local.json. SessionStart is the open-the-repo trigger; other events run during interaction. prompt-type hooks (no shell) are not flagged.
Recommended action
Don't commit type: command hooks that other contributors will execute unknowingly. Keep agent hooks in the user-level ~/.claude/settings.json or the git-ignored .claude/settings.local.json instead of the shared .claude/settings.json. If a project hook is genuinely needed, keep its command vendored and free of network fetches (DEV-004) and document it so reviewers expect it.
DEV-004: Auto-run command fetches and executes remote code
Fires when a command on an auto-execution surface (VS Code folderOpen task, devcontainer lifecycle / initializeCommand, or a Claude Code command hook) matches the remote-fetch-to-interpreter idiom catalog (curl|bash, wget|sh, bash -c "$(curl …)", PowerShell irm|iex, …). Scoped to the auto-run command strings, so an unrelated URL elsewhere in the config does not trigger it. Vendor-trusted installer hosts are still flagged (the auto-run-on-open context makes them risky) but carry a vendor_trusted marker in the detector output.
Seen in the wild
- Red Hat npm compromise second-stage loaders (BoostSecurity, "Trusted Publishing, Untrusted Branch", 2026): editor / devcontainer / agent configs that fetch-and-run on repo open.
Recommended action
Remove the network fetch from any command that runs on repo open. Vendor the script into the repository and invoke the checked-in copy, or download to a file and verify a pinned sha256 before executing. A curl | sh that runs the instant the repo is opened is arbitrary remote code execution on the developer's machine.
DEV-005: Devcontainer initializeCommand runs unsandboxed on the host
Fires whenever devcontainer.json declares an initializeCommand. That hook runs on the host before the container is created, so unlike the in-container lifecycle hooks (DEV-002) it has no container isolation. Common on legitimate setups too, hence HIGH rather than CRITICAL unless it also fetches remote code.
Recommended action
Move host-side setup into onCreateCommand / postCreateCommand so it runs inside the container, where the blast radius is the disposable devcontainer rather than the developer's workstation. Reserve initializeCommand for genuinely host-only, trusted, vendored steps, and never let it fetch and run remote code (DEV-004).
DEV-006: VS Code settings point a tool at a repo-local binary
Fires on a .vscode/settings.json that (a) sets a known executable-path key (or a go.alternateTools / terminal automation-profile path) to a repo-relative value (a path with a separator that is not absolute, or one using ${workspaceFolder}), (b) sets terminal.integrated.env.* to a process-hijack variable, or (c) enables task.allowAutomaticTasks. A bare command (git, resolved from PATH) or an absolute system path passes. VS Code Workspace Trust gates the first open, but reviewers routinely trust repos they clone. Complements DEV-001 (folder-open task), DEV-003 (committed Claude hook), and DEV-005 (devcontainer host command); this is the settings-file launch surface none of them read.
Seen in the wild
- Microsoft VS Code Workspace Trust exists precisely because a committed workspace
settings.jsoncan point tool / interpreter paths at attacker-controlled binaries that run on folder open; the 2026 npm second-stage 'open the checkout' loaders (Red Hat compromise) used the same checkout-time auto-execution class.
Recommended action
Don't commit a workspace .vscode/settings.json that points an executable-path setting (git.path, python.defaultInterpreterPath, eslint.runtime, go.alternateTools, a terminal automation profile, ...) at a repo-relative path, injects a process-hijack variable through terminal.integrated.env.* (PATH / LD_PRELOAD / NODE_OPTIONS), or sets task.allowAutomaticTasks: on. Keep tool paths pointing at system binaries (an absolute path or a bare command resolved from the user's PATH), and let each developer configure machine-specific paths in their user settings, not a committed workspace file.
DEV-007: Committed MCP config auto-launches a local command server
Fires when a committed MCP config (.mcp.json, .cursor/mcp.json, .vscode/mcp.json) defines a server with a command (a stdio server the editor / agent launches as a local process on project open). Both the mcpServers (Claude / Cursor) and servers (VS Code) block names are read. url-only servers (type: http / sse) don't spawn a local process and don't fire. Commands that fetch an unpinned remote package (npx -y / uvx / pnpm dlx / bunx / pipx run) are called out as the sharpest case.
Known false-positive modes
- A first-party MCP server invoked from a checked-in, reviewed script (
node ./tools/mcp-server.js) is intentional. The finding still flags that the config auto-launches a process on open; suppress on the file with a rationale naming the server.
Recommended action
Treat a committed MCP server config as code that runs on project open. Prefer a first-party server invoked from a checked-in, reviewed script over a npx -y / uvx runner that pulls an unpinned remote package; if a remote package is required, pin it to an exact version (and ideally an integrity hash). Keep developer-specific or untrusted MCP servers in user-level config (~/.cursor / user settings) rather than committing them to the repository where they auto-launch for every contributor.
DEV-008: Credential-shaped literal in a developer-environment config
Scans every string in a developer-environment config (.vscode/ tasks / settings, .devcontainer, .claude/settings.json, and MCP configs .mcp.json / .cursor/mcp.json / .vscode/mcp.json) against the cross-provider credential-shape catalog. The common hit is a token in an MCP server's env block or a devcontainer remoteEnv / containerEnv.
Known false-positive modes
- Documentation / example configs sometimes embed credential-shaped strings (a sample
ghp_token, a JWT). Well-known vendor example tokens are suppressed by the shared catalog; suppress a genuine fixture per-resource with a rationale.
Recommended action
Rotate the exposed credential immediately, it is in the repo's history. Don't commit secrets to editor / agent / container config: pass them through the environment at run time (an MCP server reads ${env:GITHUB_TOKEN} from the developer's shell, a devcontainer reads a host env var) or a local, git-ignored config rather than a committed literal.
Adding a new Developer environment check
- Create a new module at
pipeline_check/core/checks/devenv/rules/NNN_<name>.pyexporting a top-levelRULE = Rule(...)and acheck(path, doc) -> Findingfunction. The orchestrator auto-discoversRULEand callscheckwith the parsed YAML document. - Add a mapping for the new ID in
pipeline_check/core/standards/data/owasp_cicd_top_10.py(and any other standard that applies). - Drop unsafe/safe snippets at
tests/fixtures/per_check/devenv/-NNN.{unsafe,safe}.ymland add aCheckCaseentry intests/test_per_check_real_examples.py::CASES. - Regenerate this doc: