The package dependency management decision record: why the dependency pinning model you chose determines your reproducibility ceiling and your supply chain attack surface

Published 2026-07-18 · WhyChose

Package dependency decisions are made when a project is initialized, when the CI/CD pipeline first fails because a developer's local Node.js version resolved a different dependency tree than the build server's, and when a security advisory appears in the company Slack and the on-call engineer needs to determine whether any production service is affected. The AI session that handles project bootstrapping is task-oriented and terminates at the working application: it runs npm init or poetry new or go mod init, installs the framework packages the application requires, runs the development server, confirms that the application starts, and closes. The dependency installation works. The session succeeds. The session closes.

What the AI session does not produce is the second half of the dependency decision. The session answers "how do I install the packages this application needs?" and delivers a working node_modules directory or virtual environment. It does not ask: is the lockfile committed to the repository — package-lock.json, yarn.lock, pnpm-lock.yaml, poetry.lock, go.sum — and does the CI/CD pipeline use the lockfile's exact resolved versions via npm ci rather than resolving the dependency tree fresh on every build; what is the version constraint syntax for direct dependencies (^ minor range, ~ patch range, or exact version) and does the choice still matter if the lockfile is committed; what happens when a transitive dependency maintainer publishes a malicious patch version at 14:23 UTC, the security community issues an advisory at 15:47 UTC, and the CI/CD pipeline built at 16:02 UTC without a committed lockfile has already installed the malicious version; is there a private registry proxying public packages through a security scanning intermediary, and if so what is the fallback model when the registry is unreachable — whether builds fail immediately, fall through to the public registry bypassing the scanning controls, or hang for 30-120 seconds per package resolution; what is the approval process for dependency updates that introduce breaking changes, and does Dependabot's pull request merge policy distinguish between a patch version update that fixes a CVE in a package the application code doesn't directly call and a major version update that changes the API of a framework the application code calls on every request; what is the remediation SLA when a critical CVE is discovered in a transitive dependency for which no fixed version is available upstream. Each of these questions has an answer that is not derivable from "npm install succeeded and the development server starts" as the session completion criterion. Each answer determines whether a supply chain attack that publishes a malicious package at 14:23 gets shipped to production at 16:02 or is blocked by a lockfile that pins the safe version until the lockfile is explicitly updated with a reviewed diff, whether a private registry outage at 09:14 blocks all CI/CD pipelines until 10:01 or causes a developer to bypass registry controls with a direct --registry flag that circumvents the access logging and security scanning the registry was deployed to provide, and whether a critical CVE discovered in a transitive dependency is remediated within 24 hours by an override in package.json or sits unpatched for three months because the remediation path is undocumented. The answers are in the AI sessions. They are almost never written down.

Two ways package dependency decisions produce the wrong outcome in production

The supply chain attack via unpinned transitive dependency

A developer-tools startup builds its API in Node.js. The founding engineer runs a ChatGPT session to initialize the project: npm init -y, npm install express axios pg dotenv, creates the first route handlers, confirms that node index.js starts the server, confirms that curl returns a 200 from the health endpoint. npm install generates a package-lock.json. The founding engineer reviews the generated files, sees that package-lock.json is 1,847 lines and contains cryptographic hashes for every package in the resolved dependency tree, and adds package-lock.json to the .gitignore. The reasoning, which is not recorded, is that committing the lockfile will cause unnecessary merge conflicts when dependencies are updated. The session ends. The project works. The dependency tree is resolved fresh from the npm registry on every npm install.

Sixteen months into the company's life, a maintainer of a popular string parsing utility that is a transitive dependency of the version of axios in the project's package.json publishes version 3.8.2, replacing version 3.8.1. The maintainer's npm account has been compromised by a credential-stuffing attack that used a password reused from a 2021 data breach. Version 3.8.2 adds two lines of obfuscated JavaScript to the package's index.js that read the process.env object and send its contents to an external HTTPS endpoint. The package passes npm's automated malware scan because the malicious code path only activates when process.env.AWS_ACCESS_KEY_ID or process.env.DATABASE_URL is present — environment variables that are absent in the sandbox environment npm uses for static analysis. Version 3.8.2 is published at 14:23 UTC on a Tuesday.

At 15:47 UTC — 84 minutes later — a security researcher publishes a GitHub issue flagging the malicious code in version 3.8.2. The issue begins propagating through security Twitter and the Node.js community Slack. At 16:02 UTC, the startup's CI/CD pipeline runs a build triggered by a routine deployment. The pipeline runs npm install, which resolves the dependency tree fresh and installs version 3.8.2 of the string parsing utility — the latest version satisfying the patch range that axios's package.json specifies for the utility. The build succeeds. The production deployment completes at 16:14 UTC. At 17:31 UTC, an SRE monitoring the startup's network egress logs notices anomalous outbound HTTPS connections from the API server to an IP address not in the egress allowlist. The SRE escalates to the security team. The investigation identifies that version 3.8.2 of the string parsing utility is the source of the exfiltration attempt. The team identifies that DATABASE_URL was present in the production environment and was transmitted to the external endpoint. The incident requires immediate credential rotation for all database users, an audit of whether any credentials were successfully exfiltrated and used, and notification to affected customers under the company's breach disclosure obligations. The startup's package-lock.json, had it been committed, would have pinned the string parsing utility to 3.8.1 — the version installed the last time npm install was run and committed. The CI/CD pipeline's npm ci command would have installed exactly 3.8.1 and verified its content hash against the integrity field in the lockfile. Version 3.8.2 would not have been installed until a developer explicitly ran npm update and reviewed the lockfile diff showing the version change — a review that would have occurred after the security advisory was public.

The private registry bypass during a Kubernetes migration

A B2B SaaS company publishes internal Node.js utilities as npm packages under the @company scope. After the third time a developer accidentally published an internal package to the public npm registry with credentials visible in the package source, the platform team deploys a Verdaccio private npm registry in a Kubernetes namespace. A Claude session configures the registry: installs Verdaccio via Helm chart, configures the @company scope to publish to the private registry, adds .npmrc to the project repositories pointing @company:registry=https://registry.company.internal, verifies that npm install @company/internal-utils resolves from the private registry by checking the install log output. The session also configures Verdaccio's uplinks to proxy public npm packages through the registry for security scanning and download caching. The session ends. The private registry is working. Internal package publishing is now scoped to the private registry.

The session does not document three properties of the private registry configuration. First, the fallback behavior: when the Verdaccio registry is unavailable, npm's behavior for non-@company-scoped packages depends on the .npmrc configuration — if the global registry is set to the Verdaccio instance (registry=https://registry.company.internal), npm will fail with a connection error for all packages including public ones; if only the @company scope is redirected to Verdaccio (which is how the session configured it), non-scoped packages will continue to resolve from the public npm registry but @company packages will fail. The session documentation records "private registry deployed and configured" without specifying whether the global registry or only the @company scope is redirected. Second, the Verdaccio persistence model: Verdaccio stores its package cache in a Kubernetes PersistentVolumeClaim. The session does not document the PVC's storage class, its backup policy, or what happens to the cache if the PVC is deleted and the Verdaccio pod is restarted — whether the registry starts empty and requires a fresh pull from the public npm registry for every proxied package, or whether the cache is populated from a backup. Third, the circuit breaker documentation: what is the approved procedure when the private registry is unavailable and a production deployment is blocked?

Seven months after the private registry deployment, the platform team migrates the registry Kubernetes namespace from a self-managed cluster to a managed cluster as part of a broader infrastructure consolidation. During the migration at 09:14 UTC, the Verdaccio pod enters a CrashLoopBackOff state due to a PVC storage class mismatch. All CI/CD pipelines that run npm install and require @company-scoped packages begin failing at 09:17 UTC. At 09:41 UTC, a developer investigating the CI failures discovers that adding --registry https://registry.npmjs.org to the npm install command unblocks the build for non-@company packages, and that the @company packages can be temporarily published to a developer's local npm account and installed from there. The developer implements this workaround and pushes a commit to the CI/CD pipeline configuration. The workaround resolves the build failures. The Verdaccio registry is restored at 10:01 UTC. After the incident, a post-mortem notes that the workaround bypassed the private registry's package download audit log — the audit log that records which packages are downloaded by which pipelines and which developers, a control that the security team had cited in the SOC 2 audit as evidence of supply chain monitoring. The post-mortem action item is to document the approved fallback procedure. The action item is not completed before the next quarterly SOC 2 audit review.

Three structural properties that package dependency decisions determine

The dependency pinning model and the reproducibility ceiling

The dependency pinning model specifies the version constraint syntax for direct dependencies, whether lockfiles are committed and used in CI/CD builds, and whether the lockfile includes content-addressable checksums that detect tampered package contents after the lockfile was generated. Without a documented pinning model, the team's mental model is "we use npm and have a package.json" — a model that is accurate but does not answer whether two builds on different machines at different times produce the same dependency tree, which is the operational meaning of reproducibility. The pinning model must specify six properties. First, the version constraint syntax per package category: framework packages (express, fastapi, gin) may be pinned to minor range (^x.y.z) because framework APIs are stable within a major version and automatic patch updates reduce exposure to known CVEs, while security-critical packages (jsonwebtoken, cryptography, bcrypt) should be pinned to exact versions to prevent unexpected behavior changes from automatic updates; the rationale for each category's constraint syntax must be recorded because a future developer who sees that jsonwebtoken is pinned exactly while express uses ^ needs to understand whether the difference is intentional policy or historical accident. Second, the lockfile commitment policy: the lockfile (package-lock.json, yarn.lock, pnpm-lock.yaml, poetry.lock, go.sum) must be committed to the repository and treated as a required file in code review; the common rationale for not committing lockfiles — merge conflicts during dependency updates — is resolved by Dependabot and Renovate, which generate lockfile updates as separate pull requests with clear diffs, rather than by removing the lockfile. Third, the CI/CD build command: builds must use the lockfile's frozen install command (npm ci for npm, pip install --require-hashes -r requirements.txt for pip with hashed requirements, go build with go.sum present for Go, bundle install --frozen for Bundler) rather than the interactive install command (npm install, pip install, go get); the frozen command verifies content hashes and fails the build if the lockfile does not match the installed packages or if the lockfile is absent. Fourth, the content hash verification model: npm's package-lock.json includes an integrity field for each package containing a Subresource Integrity hash (sha512-base64) of the package tarball; npm ci verifies this hash at install time and fails if the package content does not match; yarn.lock includes a checksum field with an equivalent hash; pip's hashed requirements format (generated by pip-compile --generate-hashes) includes SHA-256 and SHA-512 hashes for each package; Go's go.sum includes SHA-256 hashes of the module directory tree at each version. The content hash is the control that detects supply chain attacks that replace a package's contents without changing its version number — a scenario distinct from a malicious new version, where the attack modifies a package that was already in the lockfile without changing the recorded version. Fifth, the lockfile update procedure: lockfile updates must be generated by an automated dependency update tool (Dependabot, Renovate) or by a deliberate npm update command, and the resulting lockfile diff must be reviewed in the pull request as part of the code review — a diff showing a transitive dependency version change from 3.8.1 to 3.8.2 is reviewable, whereas a build that silently installs 3.8.2 without producing a diff is not. Sixth, the Go module and Python virtual environment equivalents of the lockfile: Go's go.sum records the expected cryptographic hash for every module version in the dependency tree and is checked into the repository by default; pip's requirements.txt with pinned versions does not record content hashes by default but can be generated with pip-compile --generate-hashes; the pinning model must specify the equivalent control for each language runtime in use. The infrastructure decision record specifies the Node.js, Python, and Go runtime versions in use; the dependency pinning model must be consistent with the runtime version, because Node.js major versions have different npm client behaviors for lockfile handling, and Go module versioning semantics differ between Go 1.16 and later versions.

The supply chain integrity verification model and the attack surface

The supply chain integrity verification model specifies how the CI/CD pipeline verifies that the packages being installed are the packages the team reviewed, and what the response procedure is when a package in the dependency tree is identified as malicious or vulnerable after it has been installed. Without a documented verification model, the team's supply chain security posture is whatever npm, pip, or go's default behavior provides — which includes lockfile-based content hash verification (if a lockfile is committed and used) but does not include provenance attestation verification, software bill of materials generation, or a documented response procedure for supply chain incidents. The verification model must specify four properties. First, the provenance attestation verification policy: npm provenance (introduced in npm 9.5.0 and npm registry support beginning in May 2023) allows package maintainers to publish a Sigstore-signed attestation linking a package version to its source repository and CI/CD build — npm install --audit verifies these attestations when present, and the policy must specify whether the team requires provenance attestations for direct dependencies (a restrictive policy that may exclude packages whose maintainers have not configured npm provenance, including many packages maintained before 2023), or whether attestations are verified when present but not required (which provides additional assurance for packages that publish them without restricting the package selection). Second, the software bill of materials generation: a Software Bill of Materials (SBOM) is a machine-readable inventory of every package in the dependency tree at a specific build — generated by npm sbom, syft, cyclonedx-npm, or equivalent tools — that provides the complete list of packages and versions in a format compatible with vulnerability management tools and regulatory requirements (the US Executive Order on Improving the Nation's Cybersecurity requires SBOMs for software sold to the federal government, and the EU Cyber Resilience Act includes SBOM requirements for CE-marked software); the policy must specify whether SBOMs are generated per build, per release, or not at all, and where they are stored. Third, the CI/CD audit pipeline: the audit step that runs npm audit --audit-level=high (or pip-audit, govulncheck, bundler-audit) must be a blocking step in the CI/CD pipeline — a step that causes the build to fail rather than producing a warning that is logged and ignored; the audit-level parameter specifies the minimum severity that blocks the build (critical, high, moderate, or low), and the policy must justify the chosen level; running npm audit --audit-level=critical allows high-severity vulnerabilities to pass to production, which may be acceptable during periods of dependency tree instability where high-severity vulnerabilities cannot be immediately remediated, but must be a documented exception rather than an undocumented default. Fourth, the incident response procedure for a supply chain attack: the discovery of a malicious package version in a production build requires an incident response procedure that specifies who is responsible for assessment, what the assessment covers (whether the malicious code path was executed, what data may have been exfiltrated, whether credentials need to be rotated), how the malicious package is removed from the production environment (build and redeploy from a clean dependency tree with the malicious package excluded or replaced, or roll back to the last clean build artifact), and what disclosure obligations apply if customer data may have been exfiltrated. The secrets management decision record specifies which secrets are present in the production environment; the supply chain attack response procedure must cross-reference the secrets management record to determine which credentials must be rotated following a confirmed or suspected supply chain compromise. The security threat model and compliance decision record must classify supply chain attacks as in-scope threats — a threat model that covers only external network attacks and social engineering but not supply chain attacks will not have a documented control for the attack vector that reaches production through the dependency installation step of a routine CI/CD build.

The private registry model and the dependency availability surface

The private registry model specifies whether packages are proxied through a controlled intermediary, what the registry's availability model is, what the fallback behavior is when the registry is unreachable, and what controls the registry provides that the public registry does not. Without a documented registry model, the team's mental model is "we use Verdaccio for internal packages" — which does not answer what happens to builds during a registry outage, whether the registry's proxy cache is persistent across restarts, what audit logging the registry provides, or what the approved procedure is when the registry needs to be bypassed. The registry model must specify five properties. First, the registry scope and proxy configuration: which package scopes are redirected to the private registry (only @company scope, all packages, or specific package names), whether the registry proxies public packages through the private registry (providing a cached, scanned intermediary for all dependencies) or only serves internal packages (delegating public package resolution to the public registry directly), and what security scanning the registry applies to proxied packages (static analysis, license compliance checking, or blocking packages with known CVEs in the package's advisory database). Second, the availability configuration: the private registry's Kubernetes deployment configuration (replica count, pod disruption budget, PersistentVolumeClaim storage class and backup policy, horizontal pod autoscaler configuration), the registry's load balancer and health check configuration, and whether the registry has a high-availability configuration that survives a single pod failure without service interruption. Third, the fallback behavior specification: what npm does when the private registry returns a connection error (fail immediately, fall through to the public registry, or hang until timeout), how the .npmrc configuration produces each failure mode, and which failure mode is chosen and why — immediate failure prevents silent bypass of the registry's security controls but causes CI/CD outages during registry maintenance windows; public registry fallback maintains CI/CD availability during outages but requires that the registry's security controls be complemented by controls that do not depend on registry availability (such as lockfile content hash verification); the fallback behavior must be tested as part of the registry's deployment validation, not discovered during a production incident. Fourth, the approved bypass procedure: what the engineering team is authorized to do when the private registry is unavailable and a deployment is time-sensitive (roll back to the last successful build artifact from the CI/CD cache, temporarily enable direct public registry access for non-scoped packages, or delay the deployment until the registry is restored), and what security review is required after a bypass is used (audit of the packages installed during the bypass against the vulnerability database, review of the bypass scope to confirm that only public packages were accessed and no internal package content was fetched from the public registry). Fifth, the audit log model: the registry's download log must be treated as a security artifact — each log entry records which package version was downloaded, by which IP or service account, at what time — and the log retention policy, the monitoring configuration for anomalous access patterns (a package being downloaded by an IP not in the engineering office or CI/CD IP range, a package being downloaded at an unusual frequency suggesting an extraction attempt), and the log export configuration for the company's SIEM or log aggregation platform must be specified. The CI/CD pipeline decision record specifies the pipeline configuration and the build environment; the private registry's network access must be configured as an explicit allowlist in the pipeline's network egress policy, so that a developer cannot bypass the registry by adding a --registry flag to the npm install command in the CI/CD pipeline configuration without the change being visible as a network egress policy change requiring review.

Three AI session types that embed dependency decisions without documenting them

The project bootstrapping session is where the dependency pinning model is established without being named as a decision. The session is task-oriented and terminates at the working application: install the required packages, start the development server, confirm the health endpoint returns 200, and close. The package manager choice (npm versus yarn versus pnpm for Node.js; pip versus poetry versus conda for Python; go get versus go mod tidy for Go) is made based on the AI session's recommendation or the engineer's familiarity, without documenting the rationale for the choice — the choice determines the lockfile format, the frozen install command syntax, the dependency resolution algorithm, and the update mechanism, but these properties are not surfaced by the choice itself at the moment of selection. The lockfile is generated by the package manager's install command. The lockfile is either committed to the repository (if the engineer knows to commit it and does not add it to .gitignore) or not committed (if the engineer treats the generated file as build output equivalent to node_modules, which should not be committed). The version constraint syntax is whatever the package manager writes by default — npm writes ^ minor ranges, pip writes exact versions with == by default, poetry writes ^ or ~ ranges in pyproject.toml. None of these defaults are the result of a policy decision; they are the package manager's default behavior, which may or may not be appropriate for the project's security requirements. The session result is "the application works with the installed packages" — not "dependency pinning model is lockfile committed with npm ci, version constraints are ^ for framework packages and exact pinning for security-critical packages, rationale for each constraint category is recorded in the decision record." The second result requires treating the dependency configuration as a decision with security consequences rather than as infrastructure plumbing with a binary working/not-working completion criterion. The build system decision record specifies the build tool and the build configuration; the dependency management model must be consistent with the build system's dependency resolution — a build system that runs npm install in the build step rather than npm ci is not using the lockfile regardless of whether the lockfile is committed to the repository.

The CI reproducibility debugging session embeds dependency decisions as configuration changes without resolving the underlying pinning model. The session is triggered by a failure: the application works on the engineer's laptop but the CI build fails with a different error, or two engineers get different test results because their local dependency trees have diverged. The session discovers that the CI environment has a different version of a transitive dependency than the local environment — the CI build ran npm install after a new version of the transitive package was published, while the local environment's node_modules still contains the previous version from the last local npm install. The session fixes the immediate problem by committing the lockfile or by pinning the problematic dependency to an exact version. The session does not audit the full dependency tree to determine whether other transitive dependencies also diverge between environments, does not update the CI/CD configuration to use npm ci instead of npm install (which would prevent future divergence), does not document the pinning model that the fix implies (if an exact-pinned transitive dependency is added as a workaround, the workaround's maintenance burden — manually updating the pin when security patches are released for the exact-pinned version — is not documented alongside the pin). The session result is "CI is passing and the version divergence is resolved" — not "lockfile committed, CI/CD uses npm ci, direct dependencies use ^ with the following exceptions, transitive dependency overrides are listed with rationale and maintenance schedule." The new CTO onboarding problem with dependency management is that an incoming technical leader cannot determine the pinning model from package.json alone: a package.json with ^ ranges and a committed package-lock.json is using the lockfile correctly, but a package.json with ^ ranges and no committed lockfile is resolving the dependency tree fresh on every build; both produce the same package.json, and the difference is only visible by checking whether package-lock.json is in .gitignore and whether the CI/CD pipeline uses npm ci or npm install.

The supply chain security hardening session applies tool-level controls without specifying the response procedure or the audit coverage model. The session is triggered by an external event — a security advisory, a compliance requirement, a penetration test finding, or an internal security review — and terminates when the tools are configured and the immediate scan returns no critical findings. The session adds Dependabot to the repository (a .github/dependabot.yml file configuring weekly dependency update pull requests for npm dependencies), runs npm audit and marks the critical findings resolved (by running npm audit fix for auto-fixable vulnerabilities and adding npm-audit-resolve records for vulnerabilities with no available fix), and adds an npm audit --audit-level=high step to the CI/CD pipeline. The session does not specify the Dependabot merge policy (whether Dependabot's pull requests require manual approval or can be auto-merged when all CI checks pass, and whether patch version updates have a different approval policy than minor or major version updates), the audit SLA for vulnerabilities that cannot be immediately remediated (critical: 24 hours to plan remediation, 7 days to deploy fix; high: 7 days to plan, 30 days to deploy), the transitive dependency override procedure for vulnerabilities where the direct dependency has not yet released a fix, or the supply chain incident response procedure for a confirmed malicious package. The session result is "Dependabot enabled, npm audit passing, audit step added to CI" — not "dependency update policy: Dependabot auto-merge for patch versions that pass CI, manual review required for minor and major versions, merge within 7 days of PR creation for security patches; audit SLA: critical 24h/7d, high 7d/30d; transitive override procedure: add overrides field to package.json with rationale comment, update lock file, document in ADR." The observability strategy decision record specifies the monitoring and alerting platform; the supply chain security controls must be wired into the observability platform — Dependabot's vulnerability alerts must be routed to the security team's alert channel, npm audit's CI step must produce structured output that is parseable by the monitoring system, and the private registry's download audit log must be ingested by the log aggregation platform and monitored for anomalous access patterns.

The five sections of a package dependency management decision record

The first section documents the package manager selection and lockfile model. The package manager choice (npm, yarn, pnpm for Node.js; pip, poetry, conda for Python; go mod for Go; bundler for Ruby; cargo for Rust) determines the lockfile format, the frozen install command, the dependency resolution algorithm, the workspace support for monorepo configurations, and the plugin ecosystem for custom resolution behavior. The rationale for the package manager choice must be recorded because migration between package managers (from npm to pnpm, from pip to poetry) is a non-trivial change that affects every developer's local setup, every CI/CD pipeline, and every Dockerfile that runs dependency installation — a future team member evaluating whether to migrate to a package manager with better performance or security features needs to understand the original rationale to assess whether the rationale still applies. The lockfile model must specify whether the lockfile is committed (required for supply chain integrity and reproducibility), the CI/CD command that uses the lockfile's frozen resolution (npm ci, yarn install --frozen-lockfile, pnpm install --frozen-lockfile, pip install -r requirements.txt with hashed requirements, poetry install, go build), and the update procedure for the lockfile (npm update with a specified package name generates an updated lockfile for that package; Dependabot generates lockfile updates as pull requests; a manual npm update with no package specified updates all packages to the latest versions within their range constraints, which must be a deliberate action not a routine CI step). The lockfile commitment policy must also address the Go module case, where go.sum is committed by default and go mod tidy updates it automatically — the policy must specify whether go.sum updates are reviewed in pull requests or committed automatically as part of the build step. The infrastructure decision record specifies the Node.js and Python runtime versions; the package manager selection must specify the package manager version alongside the runtime version, because npm 6, npm 7, npm 8, and npm 9 have different lockfile formats (package-lock.json lockfileVersion 1, 2, and 3) and different behaviors for the --package-lock-only flag, the overrides field, and the audit command output format.

The second section documents the version pinning strategy and the dependency update policy. The version pinning strategy must specify the version constraint syntax per dependency category, the rationale for each category's constraint, and the exceptions to the category rules with individual package-level justification. Direct framework dependencies (web frameworks, ORMs, testing libraries) typically use minor range (^x.y.z) to receive automatic patch updates that include security fixes, with major version updates requiring a deliberate update and breaking change review. Security-critical direct dependencies (JSON Web Token libraries, cryptography libraries, password hashing libraries, XML parsers) are candidates for exact pinning (x.y.z) because the security properties of these packages may change between patch versions, and the risk of an unexpected behavior change in a security-critical package outweighs the operational overhead of manual patch version updates. The dependency update policy must specify the Dependabot or Renovate configuration: the update frequency (daily, weekly, or monthly), the grouping policy (whether patch updates across all packages are grouped into a single pull request or separated by package), the auto-merge policy (whether pull requests that pass all CI checks are automatically merged or require manual approval), and the staleness policy (whether pull requests that have been open for more than N days without merge are automatically closed or escalated). The update policy must distinguish between security updates (which should have a shorter approval and merge cadence, regardless of the update frequency setting — Dependabot can be configured to prioritize security updates with a separate schedule from routine version updates) and routine version updates (which can be batched and reviewed on a weekly cadence). The major version update policy must specify the required testing before merge: at minimum, the full automated test suite in a staging environment that mirrors production traffic patterns, with a review of the package's breaking change changelog identifying any behavioral changes that affect code paths used by the application. The build system decision record specifies the test configuration; the dependency update testing requirements reference the test suite's coverage as the evidence that a dependency update does not introduce a regression.

The third section documents the supply chain integrity verification model and the private registry configuration. The supply chain integrity model must specify the verification controls applied to packages at install time (lockfile content hash verification via npm ci, hashed requirements via pip install --require-hashes, go.sum hash verification via go build), the provenance attestation policy (whether npm provenance attestations are required for direct dependencies, and what the policy is for packages that do not publish attestations), and the SBOM generation requirement (whether a Software Bill of Materials is generated per build or per release, the format — CycloneDX, SPDX, or SWID — the storage location, and the retention period). The private registry configuration must specify the registry URL and scope configuration in .npmrc or pip.conf, the proxy configuration (whether public packages are proxied through the private registry or resolved directly from the public registry), the registry's availability model (replica count, PVC storage configuration, health check configuration), the fallback behavior when the registry is unavailable (fail immediately, fall through to public registry for non-scoped packages, or hang until timeout), and the approved bypass procedure when the registry is unavailable and a deployment cannot wait for restoration. The audit log model must specify the registry's download log format, the log retention policy, the monitoring configuration for anomalous access patterns, and the log export target. If no private registry is used, the section must state that explicitly with the rationale — whether the team's package selection is sufficiently narrow and the packages sufficiently high-profile that supply chain attack risk is acceptable without a proxying intermediary, or whether the cost and operational overhead of maintaining a private registry is not justified by the current scale. The TLS and certificate management decision record specifies the TLS configuration; the private registry's TLS certificate must be included in the certificate provisioning model, and the registry's client certificate configuration (if mutual TLS is used for CI/CD pipeline authentication to the registry) must be specified alongside the mTLS model for service-to-service authentication.

The fourth section documents the dependency audit and vulnerability response procedure. The audit procedure must specify the audit tool per runtime ecosystem (npm audit for Node.js, pip-audit or safety for Python, govulncheck for Go, bundler-audit for Ruby, cargo audit for Rust), the audit command and flags used in the CI/CD pipeline (npm audit --audit-level=high --json, pip-audit --format=json --output=/tmp/audit.json, govulncheck ./...), whether the audit step is a blocking step that fails the build on findings above the severity threshold or a non-blocking step that produces a report, and the severity threshold with rationale. The vulnerability response SLA must specify the target remediation time per severity tier: for critical vulnerabilities (CVSS score >= 9.0), the SLA should specify a planning deadline (within 24 hours of discovery, the team has a documented remediation plan) and an implementation deadline (within 7 days, the fix is deployed to production); for high-severity vulnerabilities (CVSS 7.0-8.9), the planning deadline should be 7 days and the implementation deadline 30 days; for medium and low severities, the response can be batched into the normal dependency update cycle with a maximum age before they must be addressed. The transitive dependency remediation procedure must specify the options available when a transitive dependency has a known vulnerability and the direct dependency that includes it has not yet released a fix: using the package manager's override mechanism (npm's overrides field in package.json for npm 8.3+, yarn's resolutions field, poetry's overrides section) to force a specific version of the transitive dependency, noting that overrides may break the direct dependency's behavior if the overridden version has a different API, and that each override must be documented in the ADR with the CVE it addresses and the date it can be removed (when the direct dependency releases a version that includes the fixed transitive dependency). The exception procedure must specify the approval process for accepting a vulnerability as acceptable risk when no fix is available and the vulnerable code path is not reachable from the application's usage of the package: who can approve an exception (the security lead or the CTO, not an individual engineer), what evidence is required (a documented analysis of the code path showing that the vulnerable function is not called with untrusted input), and what the maximum duration of an exception is before it must be re-evaluated. The container orchestration decision record specifies the base image and the container build process; the dependency audit model must include the OS package layer of the container (apt or yum packages installed in the Dockerfile) alongside the language package layer (npm or pip packages installed in the Dockerfile), because vulnerabilities in the OS package layer — in OpenSSL, libc, curl, or other system libraries installed via apt-get — are not audited by npm audit or pip-audit and require a separate container scanning tool (Trivy, Snyk Container, Docker Scout) to detect.

The fifth section documents the build environment and runtime version management. The build environment specification must identify every artifact installed during the CI/CD build that is not a package manager dependency: the Node.js or Python or Go runtime version, the package manager version (npm 9.x, yarn 3.x, poetry 1.x), any globally installed npm or pip packages used as build tools (typescript, webpack, eslint installed via npm install -g), the base Docker image version if the build runs in a container, and any system packages installed via apt-get or yum in the CI/CD environment. Each of these is a dependency that is not tracked by the package manager's lockfile and not audited by the package manager's audit command. The runtime version management policy must specify how the Node.js or Python version is pinned (via .nvmrc or .node-version for Node.js, pyproject.toml's python version requirement or .python-version for Python), what the update cadence is for the runtime version (major runtime version updates — Node.js 20 to Node.js 22, Python 3.11 to 3.12 — require a deliberate migration with testing, separate from routine package updates), and what the end-of-life policy is for the runtime version (Node.js LTS versions reach end-of-life 30 months after their initial release; Python 3.x versions are supported for approximately 5 years; continuing to use an end-of-life runtime version after the vendor stops releasing security patches creates a vulnerability surface that is not addressed by package manager audit tools). The globally installed build tool management policy must specify whether globally installed tools are also pinned to exact versions (npm install -g typescript@5.4.5 rather than npm install -g typescript), whether their versions are tracked in a version inventory file (a .tool-versions file for asdf, a Makefile with install targets, or a requirements-dev.txt for Python build tools), and whether the CI/CD pipeline's build environment is defined by a Dockerfile that pins all these versions explicitly (enabling reproducible CI/CD builds independent of the CI provider's default runtime environment). The zero trust network access decision record specifies the trust model for internal services; the supply chain is a trust boundary in the zero trust model — a package downloaded from a registry and installed in a CI/CD build environment is implicitly trusted to execute arbitrary code during the build step, and the controls that limit the blast radius of a malicious package (build environment isolation, network egress restrictions that prevent exfiltration from the build environment, process isolation that prevents the build environment from accessing production credentials) are properties of the build environment specification that must be documented alongside the dependency pinning model. The new CTO onboarding problem with dependency management is that the incoming technical leader can read package.json and understand the direct dependencies, but cannot determine whether the CI/CD pipeline uses npm ci or npm install, whether package-lock.json is in .gitignore or committed, whether the private registry has a documented fallback model, or whether the npm audit step is blocking or advisory — properties that are the difference between a supply chain attack vector that reaches production and one that is detected and blocked before the malicious build artifact is deployed. WhyChose's open-source extractor surfaces the project bootstrapping sessions, the CI reproducibility sessions, and the supply chain hardening sessions from AI chat history before an unpinned transitive dependency allows a compromised package maintainer's malicious patch version to ship to production 39 minutes after it was published, before a private registry outage causes a developer to bypass the registry's security controls with a --registry flag that persists in the CI/CD configuration long after the outage is resolved, and before a critical CVE in a transitive dependency sits unpatched for 11 weeks because the remediation procedure was not documented in the session that configured the audit tool.

The project bootstrapping session that ran npm install and confirmed that the development server starts, the CI debugging session that committed the lockfile to resolve a version divergence but did not update the CI command from npm install to npm ci, and the supply chain hardening session that added Dependabot and npm audit without specifying the remediation SLA or the transitive override procedure — each produced dependency decisions whose operational consequences (a supply chain attack that reached production 39 minutes after publication because the lockfile was not committed, a private registry bypass that circumvented three months of audit logging history because the fallback procedure was undocumented, a critical CVE in a transitive dependency that remained in production for 11 weeks because the remediation procedure was not documented alongside the audit tooling) exceed what a lockfile commitment policy specifying npm ci in CI/CD, a version constraint policy documenting exact pinning for security-critical packages, a private registry fallback model specifying the approved bypass procedure, and a dependency audit SLA specifying 24-hour planning and 7-day deployment deadlines for critical vulnerabilities would have cost to specify at the time the founding dependency decisions were made. The decisions are in the AI sessions: the package manager choice made based on the AI's recommendation without documenting the rationale that must be evaluated before migrating to a different package manager, the lockfile committed or gitignored without recording why, the version constraint syntax accepted from the package manager's default without specifying whether the default matches the project's security requirements for each dependency category, the private registry deployed without documenting what happens to CI/CD builds during a registry outage. WhyChose's open-source extractor surfaces these bootstrapping sessions, debugging sessions, and security hardening sessions as structured decision records before a supply chain attack makes the absence of a lockfile's content hash verification the most expensive undocumented default in the project's history, before a private registry outage makes the fallback behavior's undocumented status the control gap that appears in the next SOC 2 audit, and before a transitive dependency CVE with no upstream fix makes the absence of a documented override procedure and remediation SLA the reason the vulnerability stays in production longer than the organization's stated risk tolerance permits. The decisions never written down in a dependency management setup are the lockfile commitment rationale that determines whether supply chain attacks must be noticed by a security researcher before they can be blocked, the private registry fallback specification that determines whether a 47-minute registry outage is a recoverable CI delay or a security control gap, and the dependency audit SLA that determines whether a critical CVE in a transitive dependency is the kind of finding that blocks the next deployment or the kind that ages quietly in a Jira backlog until the next penetration test surfaces it.

Further reading