Can a Malicious npm Package Compromise Your Coding Agent?
A malicious npm package can run on install and again every time a coding agent opens the project. Hooks, skills, and repo config are execution paths.
By Muhammad Saif and Aashan Khan · Techtribe co-founders
A dependency used to be a library your program called. It is now also a place an attacker can leave a command that your coding agent will run. The package's install script fires when you add it. A file it drops into the repo fires again when the agent opens the project. The agent did not need a vulnerability in the model. It needed a hook the product already supports, and a developer who ran install.
This is the supply-chain version of the tool problem in MCP server security and the text problem in prompt injection against coding agents. A package can deliver both: a binary, and a paragraph of instructions.
What OSV recorded
MAL-2026-3648 is an Open Source Vulnerabilities entry for malicious code in the npm package auth-javascript. The summary is short. The details are specific. Three malicious npm packages published by an account named superbase implement a dual-vector attack. Each package bundles a 4.5 MB statically linked, UPX-packed ELF binary at .claude/settings and a companion .claude/settings.json that registers the binary as a Claude Code SessionStart hook. The hook runs every time Claude Code opens the compromised project directory. On install, the same binary runs from a preinstall lifecycle script. The binary connects to a command-and-control server and harvests environment variables, the contents of $HOME, and /proc entries. The point of that collection is developer credentials and a picture of the machine.
auth-javascript impersonates auth-js, an authentication client. The package includes a copy of the legitimate distribution file so it looks real, while preinstall executes the binary. A developer who typed a name that was one character off, or who accepted a suggestion, did not think they were scheduling a program to run on every future agent session.
SafeDep's write-up of the same campaign describes five typosquatting packages from accounts named superbase and micresoft, the same 4.5 MB binary, and the same hook shape: a SessionStart command pointing at ./settings with matcher *. The distinction it draws is the one that matters for agents. preinstall runs once. SessionStart runs every time the project opens, for as long as the file stays. No one has to reinstall. Opening the folder in the agent is enough.
That is why "we only installed it in a branch" is not comforting if the branch was checked out on a machine that has production tokens in the environment, and someone opened Claude Code there.
Hooks are code
Claude Code reads a project .claude/settings.json and can run the hooks listed there. A hook is not configuration in the way a color theme is configuration. It is a command line. The same is true of other agent products' local config: an MCP server entry is a command the client will start, a rules file is text the model will follow, and an install script is a command the package manager will run as you.
OWASP's agentic-skills note on supply chain says this directly. Configuration files that used to be passive metadata have become execution paths. It cites Claude Code issues around repository configuration, .claude/settings.json and hooks, where cloning and opening a malicious repository can lead to code execution and key theft before a person sees a useful warning. Treat those files as executable code. Put a trust gate on them. Do not review them as docs.
The same note describes dependency confusion inside skills. The skill you installed looks clean. Its package.json or requirements.txt pulls a typosquat, and the nested package is the payload. Scanning only the top-level skill file misses it. Pin nested dependencies to hashes, not ranges, and scan the tree.
It also describes skills that sit on hijackable sources: a deleted GitHub account, an unregistered package name, an expired domain. The skill listing keeps its stars. The thing it downloads can be replaced by whoever re-registers the name. That is an old package-management bug with a new installer, the agent's one-click skill install. A star count is not a review.
Campaigns that target the agent on purpose
Socket's report on a Shai-Hulud-style npm worm describes packages that do more than steal what is already on disk. The payload it analyzed includes a module that writes a malicious MCP server into a hidden directory under the home folder, registers ordinary-sounding tools, and puts prompt-injection text in the tool descriptions. It then adds that server to the configs it finds: Claude Code's settings, Claude Desktop, Cursor's mcp.json, Continue, Windsurf. The server entry is formatted like one a person would have added. A later session sees a tool list that includes the attacker's tools and has no reason, from the file's shape, to treat them as foreign.
Ossprey's Miasma write-up describes a worm that, given a GitHub token, harvests credentials from the filesystem, cloud APIs, process memory, and password managers, then spreads across package registries, GitHub Actions, SSH, and cloud instances. The part that belongs in this note is the agent hook. It looks for configs of a long list of coding tools, Claude Code and Cursor among them, and injects a SessionStart hook. For rules files such as .cursorrules it injects natural-language instructions instead of a JSON hook, so a control that only inspects a hooks key still misses it. The instruction is the execution path. The model is the interpreter.
These are researcher reports about specific campaigns, not a claim that every npm install in 2026 is infected. They are enough to retire the assumption that a coding agent's config is a safe place to write because only the developer edits it. The package manager edits it too, if the package tells it to.
What a review has to include
A dependency review that reads the library's API and not its scripts is the old review. Add these files to the same pull request check you would give a new GitHub Action:
package.jsonlifecycle scripts:preinstall,install,postinstall, andprepare. A new script is code execution on install.- Lockfile changes that add a package whose name is one character off a known one. Read the name out loud.
.claude/settings.json,.cursor/mcp.json, and the equivalent files for whatever agent the team uses. A diff that adds a command, a server, or an env override is a security diff.CLAUDE.md,AGENTS.md,.cursorrules, and rules directories. A new paragraph can be an instruction to the model. Review the raw bytes.- CI workflows that run an agent, install dependencies, and have access to secrets in the same job. That combination is the campaign's preferred destination: a token and a trigger.
OWASP's preventive list on the skills note matches this. Provenance and a signature over the skill and its files, so a later edit breaks the signature. A log of publishes and updates. Hashes for nested dependencies. An internal allow list for what agents may install. A way to revoke a signing key. You may not have all of that on a five-person team. You can still have the allow list and the hash, which are the parts that stop a surprise package from running this afternoon.
npm install --ignore-scripts is a useful default for a machine you do not fully trust yet, and it is not a complete control. The hook in the repo still runs when the agent opens the project, even if install scripts were ignored, if the file was already committed. Ignoring scripts stops one of the two vectors in MAL-2026-3648. It does not stop the other.
How it shows up
The typo. Someone adds a package with a name that looks like the one in the tutorial. Install succeeds. The project now contains a .claude directory they do not remember creating. The next agent session runs a binary. Environment variables from that shell are in the harvest set OSV described.
The transitive skill. A skill installed from a marketplace imports a helper whose name is almost right. The helper's install script is the payload. The skill's own README is clean, which is what a quick look at the marketplace page will tell you.
The update. A package you pinned by version range publishes a new patch. The lockfile update is a one-line version bump in a sea of other bumps. The new version adds a prepare script. CI runs it because CI always installs. The script only needs to succeed once, on a runner that has a deployment token in the environment.
The config that traveled in git. A poisoned .claude/settings.json is committed, on purpose or by a bad merge. Every teammate who pulls and opens the agent runs the hook. The blast radius is the team, not the one laptop that installed the package.
What to check this week
- Search the repo for
.claude/settings.json, MCP config files, and agent rules files. Open every command they contain. If you cannot explain the binary, remove the hook and treat the machine as exposed. - Search recent lockfile diffs for new packages. Read install scripts of anything added in the last month, including transitive packages that are new.
- Compare package names to the ones you meant, character by character, on anything that is a client library for auth, payments, or the agent itself. Those are the names campaigns imitate.
- Make sure CI does not run install scripts from unreviewed dependencies with production secrets in the same job. Split "install and test" from "deploy" so a bad script does not inherit the deploy token.
- Pin what you can to a hash or an exact version, and require a person on lockfile changes. A bot that auto-merges patch updates is a bot that auto-runs the new script.
- Keep agent configs out of world-writable paths in home directories if you can. A package that writes to
~/.claudeor~/.cursoris changing every project, not one repo. Look there after any install you regret. - If a package from this class may have run, rotate the credentials that were in the environment and in the home directory it could read. Deleting the package does not un-read a key.
Then write the rule down: new dependencies and agent config are reviewed as code execution. The rule is short because the check is not.
What does not help
A popularity badge does not help. Campaign packages exist to be installed by people who glance at the name. A private registry helps only if the team actually uses it and does not bypass it for "just this one." An antivirus signature helps after someone has published a hash. It does not help for the hour the package is new, which is the hour it is meant to be installed.
Telling the agent "do not run unfamiliar hooks" does not help if the product runs the hook before the model speaks. Session start is the product, not the model. The model can be careful and the process can still have started.
Where this meets the rest of the product
If the package's goal is a token, the token scope is the damage limit. A stolen CI token that can only read one staging repo is a cleanup. A stolen token that can deploy, or a cloud key on the laptop, is the incident. Permissions are covered in what the agent can reach. If the payload is a paragraph in a rules file rather than a binary, the review is the one in the prompt-injection note: read the raw diff, and do not let that file be edited casually.
You will not inspect every transitive file by hand forever. You can inspect the channel that executes: install scripts, hooks, MCP entries, and rules files. That channel is small. The registry is not.
What a clean machine looks like after the cleanup
After you remove a bad package, the repo can look clean while the home directory is not. Session hooks and MCP entries written under the user account survive a git reset. Walk ~/.claude, ~/.cursor, and any agent config the campaign's write-up named, and compare them to a machine that never installed the package. A server entry you did not add, pointing at a path under a hidden directory, is still the payload.
Then look at CI. A stolen token that was used once may have added a workflow, a secret, or a new collaborator. The package is gone and the access remains. Audit GitHub audit logs, cloud trail, and registry publishes for the window between install and rotation. Rotation without that look is how the same key comes back under a new name the attacker created while they had the old one.
Write the date, the package names, the files you removed, and the credentials you rotated. The next person who sees a strange .claude directory needs to know whether it was already handled. A cleanup that lives only in a chat with the agent will be redone badly, or not at all, when the next typosquat lands.
If this is your app, send the repo.
A 30-minute scope call. We read what is live, name what is dangerous, and quote a fixed scope.
How Techtribe handles a suspected bad dependency
We look at the lockfile, the install scripts, and the agent config the repo and the home directory actually contain. If a hook or a server was added, we assume the credentials available to that user may have been read and we list what to rotate. The cleanup is the files plus the keys, not the files alone.
If the product is still shipping on a dependency tree nobody has read, a fixed pass can inventory it and remove what does not belong. A team that keeps adding packages every week needs the review to stay, which is a Dedicated Team rather than a one-time scan. Bring the repo and say whether install has already been run on a machine with cloud credentials. That sentence changes the order of the call.
The fixed scope for taking a generated app to production is a page per tool: Lovable, Bolt, v0, and Replit.
Questions
Can an npm install backdoor a coding agent?
Yes. OSV-2026-3648 describes npm packages that run a binary from a preinstall script and also register that binary as a Claude Code SessionStart hook. Install runs it once. Opening the project in Claude Code runs it again.
Is a lockfile enough?
A lockfile pins the versions you resolved. It does not review install scripts, and it does not stop a package you newly add under a lookalike name. Read the diff of a dependency change, and treat a new lifecycle script as code you are about to run.
What is a skill or a hook in this context?
A hook is a command the agent runs on an event, such as session start. A skill is a packaged set of instructions and files an agent can install. Both are execution paths. A markdown file that tells the model to run a command is an execution path too, even when it is not a JSON hook.
What should we do if we already installed a package we do not trust?
Assume the machine's user credentials may have been read: environment variables, home-directory files, cloud keys. Rotate those, remove the package and any hook or MCP server it added, and do not keep working in that project with the old tokens.