{"course":{"slug":"how-software-changed","title":"How Software Development Changed","url":"https://addaly.com/learn/how-software-changed","level":"intermediate","track":"work"},"lesson":{"slug":"reviewing-dependency-changes","title":"Reviewing a dependency change","url":"https://addaly.com/learn/how-software-changed/reviewing-dependency-changes","position":17,"of":87,"minutes":9,"answer":"A one-line dependency addition is an unbounded commitment to run other people's code with your permissions, so it earns the two-minute check (existence, publisher, transitive count, install scripts, licence.","keyIdea":"A one-line dependency addition is an unbounded commitment to run other people's code with your permissions, so it earns the two-minute check (existence, publisher, transitive count, install scripts, licence, maintenance) that no other line in a diff needs."},"body":{"format":"markdown","text":"## One line, unbounded consequence\n\n```diff\n   \"dependencies\": {\n+    \"date-fns-tz-utils\": \"^1.2.0\",\n```\n\nThat is a nine-word diff and it is the largest change in the pull request. Adding a dependency means executing somebody else's code, with your permissions, in your build, plus everything that package depends on, plus everything those depend on, for as long as the package remains installed and every time it updates.\n\nDependency lines get less review attention than any other line in a diff, and they always did. Two things changed. Suggestions to install a package now arrive constantly and casually. And the package suggested may not exist, which is a failure mode the ecosystem never had before and which attackers have already industrialised: register the plausible name a model keeps inventing, wait for installs. The practice picked up the name slopsquatting.\n\n## The two-minute check\n\nBefore any new dependency lands, answer six questions. It takes two minutes and it is the highest return per minute available in review.\n\n**Does it exist, and is it the one you think?** Open the registry page. Not the search result, the page. Check the exact spelling: `python-dateutil` and `dateutil`, `crossenv` and `cross-env`, singular and plural forms, hyphens and underscores. Typosquatting predates all of this and remains the most common attack.\n\n**Who publishes it, and since when?** A package first published three weeks ago with 40,000 weekly downloads is a signal, not a recommendation. So is a package whose only maintainer account was created the same month.\n\n**How many transitive dependencies come with it?**\n\n```bash\nnpm view <pkg> dependencies\nnpm ls --all <pkg> | wc -l          # after install, in a scratch clone\npipdeptree -p <pkg>                 # Python\ngo mod graph | grep <module>        # Go\n```\n\nA utility that pulls in sixty packages to format a date is a decision about your whole supply chain, made to avoid writing nine lines.\n\n**Does it run code at install time?** `npm install` executes `preinstall`, `install` and `postinstall` scripts by default. This is the most direct path from a malicious package to your laptop and your CI credentials.\n\n```bash\nnpm view <pkg> scripts\nnpm ci --ignore-scripts        # and see whether anything actually breaks\n```\n\n**What licence is it, and does that matter here?** Copyleft licences in a distributed product, and licences that changed at version 4, are the two that catch teams out.\n\n**Is it maintained?** Last release, open issue count, number of people with commit rights. A single-maintainer package is not disqualifying (many essential ones are) but it is a fact you should know rather than discover during an incident.\n\n## Read the lockfile diff, at least the shape of it\n\nA one-line change to `package.json` can produce two thousand lines in `package-lock.json`, and nobody reads those. You do not have to. You have to look at the summary:\n\n```bash\n# how many packages were added, and which are new to the tree?\ngit diff --stat -- package-lock.json\nnpm ls --all --json | jq -r '..|.name?//empty' | sort -u > after.txt\n# compare against the same list from main\n```\n\nWhat you are looking for is a count. Adding one direct dependency and 200 transitive ones is a thing the reviewer should know before approving, and it is invisible in the human-readable part of the diff.\n\n## Free tooling, which is most of it\n\n- `npm audit`, `pip-audit`, `govulncheck`, `cargo audit`: vulnerability scans, all free, all runnable in CI in one line.\n- **OSV** (osv.dev) and **deps.dev**: free databases of known vulnerabilities and dependency graphs, queryable from a browser or an API, no account.\n- **Dependabot** and **Renovate**: free automated update pull requests. Renovate runs self-hosted if you cannot use the hosted one.\n- `npm ci --ignore-scripts` and, in Python, installing only from wheels, both reduce install-time execution.\n- A lockfile, committed, with `npm ci` rather than `npm install` in the pipeline. This is the single most effective free control on this list and many teams still do not have it.\n\n## The question that resolves most cases\n\nBefore adding anything, ask: **what would this look like if we wrote it ourselves?**\n\nFor a date-formatting helper the answer is often twelve lines and no supply chain at all. For a cryptography library, a database driver, or an HTTP client, the answer is that writing it yourself would be far worse, and you should absolutely take the dependency.\n\nThe rule is not \"fewer dependencies\". It is that the trade is between code you maintain and code you trust, and that trade should be made deliberately rather than as a side effect of a suggestion you accepted without reading."},"check":{"question":"A pull request adds a package that the registry shows was first published eleven days ago, has one maintainer, and installs cleanly with the expected API. What is the strongest reason to pause before approving?","options":[{"text":"New packages have not had time to fix bugs, so the API is likely to change and force rework later.","correct":false,"tempting":"A real maintenance concern, but it describes future inconvenience rather than the immediate risk of executing unknown code."},{"text":"One maintainer means the package will be abandoned, which creates a dependency the team will eventually have to replace.","correct":false,"tempting":"Single maintainership is common among essential packages; it is a fact to record, not by itself grounds to refuse."},{"text":"A working API proves nothing about intent, and a freshly registered plausible name is the squatting profile.","correct":true},{"text":"Any new dependency should be refused unless the same functionality cannot be written in-house, because supply chain risk always outweighs convenience.","correct":false,"tempting":"Turns a judgement about trade-offs into a blanket rule that would also refuse cryptography and database drivers, which is strictly worse."}]},"prev":{"slug":"safety-that-went-missing","title":"Safety that quietly went missing","url":"https://addaly.com/learn/how-software-changed/safety-that-went-missing"},"next":{"slug":"review-as-knowledge-transfer","title":"The second job of review, and how it broke","url":"https://addaly.com/learn/how-software-changed/review-as-knowledge-transfer"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Reviewing a dependency change', in How Software Development Changed. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/how-software-changed/reviewing-dependency-changes (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Reviewing a dependency change","courseTitle":"How Software Development Changed","module":{"index":2,"slug":"reading-what-you-did-not-write","title":"Reading what you did not write"},"answer":null,"keyIdea":"A one-line dependency addition is an unbounded commitment to run other people’s code with your permissions, so it earns the two-minute check (existence, publisher, transitive count, install scripts, licence, maintenance) that no other line in a diff needs.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"one-line-unbounded-consequence","runs":[{"text":"One line, unbounded consequence"}]},{"t":"code","lang":"diff","label":"Diff","text":"   \"dependencies\": {\n+    \"date-fns-tz-utils\": \"^1.2.0\","},{"t":"p","runs":[{"text":"That is a nine-word diff and it is the largest change in the pull request. Adding a dependency means executing somebody else’s code, with your permissions, in your build, plus everything that package depends on, plus everything those depend on, for as long as the package remains installed and every time it updates."}]},{"t":"p","runs":[{"text":"Dependency lines get less review attention than any other line in a diff, and they always did. Two things changed. Suggestions to install a package now arrive constantly and casually. And the package suggested may not exist, which is a failure mode the ecosystem never had before and which attackers have already industrialised: register the plausible name a model keeps inventing, wait for installs. The practice picked up the name slopsquatting."}]},{"t":"h2","id":"the-two-minute-check","runs":[{"text":"The two-minute check"}]},{"t":"p","runs":[{"text":"Before any new dependency lands, answer six questions. It takes two minutes and it is the highest return per minute available in review."}]},{"t":"p","runs":[{"text":"Does it exist, and is it the one you think?","b":true},{"text":" Open the registry page. Not the search result, the page. Check the exact spelling: "},{"text":"python-dateutil","code":true},{"text":" and "},{"text":"dateutil","code":true},{"text":", "},{"text":"crossenv","code":true},{"text":" and "},{"text":"cross-env","code":true},{"text":", singular and plural forms, hyphens and underscores. Typosquatting predates all of this and remains the most common attack."}]},{"t":"p","runs":[{"text":"Who publishes it, and since when?","b":true},{"text":" A package first published three weeks ago with 40,000 weekly downloads is a signal, not a recommendation. So is a package whose only maintainer account was created the same month."}]},{"t":"p","runs":[{"text":"How many transitive dependencies come with it?","b":true}]},{"t":"code","lang":"bash","label":"Bash","text":"npm view <pkg> dependencies\nnpm ls --all <pkg> | wc -l          # after install, in a scratch clone\npipdeptree -p <pkg>                 # Python\ngo mod graph | grep <module>        # Go"},{"t":"p","runs":[{"text":"A utility that pulls in sixty packages to format a date is a decision about your whole supply chain, made to avoid writing nine lines."}]},{"t":"p","runs":[{"text":"Does it run code at install time?","b":true},{"text":" "},{"text":"npm install","code":true},{"text":" executes "},{"text":"preinstall","code":true},{"text":", "},{"text":"install","code":true},{"text":" and "},{"text":"postinstall","code":true},{"text":" scripts by default. This is the most direct path from a malicious package to your laptop and your CI credentials."}]},{"t":"code","lang":"bash","label":"Bash","text":"npm view <pkg> scripts\nnpm ci --ignore-scripts        # and see whether anything actually breaks"},{"t":"p","runs":[{"text":"What licence is it, and does that matter here?","b":true},{"text":" Copyleft licences in a distributed product, and licences that changed at version 4, are the two that catch teams out."}]},{"t":"p","runs":[{"text":"Is it maintained?","b":true},{"text":" Last release, open issue count, number of people with commit rights. A single-maintainer package is not disqualifying (many essential ones are) but it is a fact you should know rather than discover during an incident."}]},{"t":"h2","id":"read-the-lockfile-diff-at-least-the-shape-of-it","runs":[{"text":"Read the lockfile diff, at least the shape of it"}]},{"t":"p","runs":[{"text":"A one-line change to "},{"text":"package.json","code":true},{"text":" can produce two thousand lines in "},{"text":"package-lock.json","code":true},{"text":", and nobody reads those. You do not have to. You have to look at the summary:"}]},{"t":"code","lang":"bash","label":"Bash","text":"# how many packages were added, and which are new to the tree?\ngit diff --stat -- package-lock.json\nnpm ls --all --json | jq -r '..|.name?//empty' | sort -u > after.txt\n# compare against the same list from main"},{"t":"p","runs":[{"text":"What you are looking for is a count. Adding one direct dependency and 200 transitive ones is a thing the reviewer should know before approving, and it is invisible in the human-readable part of the diff."}]},{"t":"h2","id":"free-tooling-which-is-most-of-it","runs":[{"text":"Free tooling, which is most of it"}]},{"t":"ul","items":[[{"text":"npm audit","code":true},{"text":", "},{"text":"pip-audit","code":true},{"text":", "},{"text":"govulncheck","code":true},{"text":", "},{"text":"cargo audit","code":true},{"text":": vulnerability scans, all free, all runnable in CI in one line."}],[{"text":"OSV","b":true},{"text":" (osv.dev) and "},{"text":"deps.dev","b":true},{"text":": free databases of known vulnerabilities and dependency graphs, queryable from a browser or an API, no account."}],[{"text":"Dependabot","b":true},{"text":" and "},{"text":"Renovate","b":true},{"text":": free automated update pull requests. Renovate runs self-hosted if you cannot use the hosted one."}],[{"text":"npm ci --ignore-scripts","code":true},{"text":" and, in Python, installing only from wheels, both reduce install-time execution."}],[{"text":"A lockfile, committed, with "},{"text":"npm ci","code":true},{"text":" rather than "},{"text":"npm install","code":true},{"text":" in the pipeline. This is the single most effective free control on this list and many teams still do not have it."}]]},{"t":"h2","id":"the-question-that-resolves-most-cases","runs":[{"text":"The question that resolves most cases"}]},{"t":"p","runs":[{"text":"Before adding anything, ask: "},{"text":"what would this look like if we wrote it ourselves?","b":true}]},{"t":"p","runs":[{"text":"For a date-formatting helper the answer is often twelve lines and no supply chain at all. For a cryptography library, a database driver, or an HTTP client, the answer is that writing it yourself would be far worse, and you should absolutely take the dependency."}]},{"t":"p","runs":[{"text":"The rule is not “fewer dependencies”. It is that the trade is between code you maintain and code you trust, and that trade should be made deliberately rather than as a side effect of a suggestion you accepted without reading."}]}],"check":{"question":[{"text":"A pull request adds a package that the registry shows was first published eleven days ago, has one maintainer, and installs cleanly with the expected API. What is the strongest reason to pause before approving?"}],"options":[{"text":[{"text":"New packages have not had time to fix bugs, so the API is likely to change and force rework later."}],"correct":false,"tempting":[{"text":"A real maintenance concern, but it describes future inconvenience rather than the immediate risk of executing unknown code."}]},{"text":[{"text":"One maintainer means the package will be abandoned, which creates a dependency the team will eventually have to replace."}],"correct":false,"tempting":[{"text":"Single maintainership is common among essential packages; it is a fact to record, not by itself grounds to refuse."}]},{"text":[{"text":"A working API proves nothing about intent, and a freshly registered plausible name is the squatting profile."}],"correct":true,"tempting":null},{"text":[{"text":"Any new dependency should be refused unless the same functionality cannot be written in-house, because supply chain risk always outweighs convenience."}],"correct":false,"tempting":[{"text":"Turns a judgement about trade-offs into a blanket rule that would also refuse cryptography and database drivers, which is strictly worse."}]}],"order":[3,1,0,2],"why":null},"prev":{"slug":"safety-that-went-missing","title":"Safety that quietly went missing","minutes":9},"next":{"slug":"review-as-knowledge-transfer","title":"The second job of review, and how it broke","minutes":9}}}