Poisoned code: what to look for in a change

When agents write most of the code, a malicious line no longer needs a malicious author. It can arrive through a package an agent imagined, an instruction file it obeyed, or a lockfile nobody opens. These are the ways new code is poisoned, how each one looks in a diff, and what visualdiff does about it.

Data leaving the building

The most common malicious change is quiet: read something valuable, send it somewhere. It often arrives labelled as telemetry, analytics or error reporting.

Environment and secrets sent out

In a diff
A "telemetry" module that serialises process.env or os.environ, or reads variables named TOKEN or SECRET, then POSTs them, sometimes base64-encoded first.
What visualdiff does
Connects the read to the send within the same file, names both lines, and holds the change when the data is environment or credentials going anywhere but your own domains.

threat.exfiltration

Credential files read

In a diff
Paths like ~/.ssh/id_rsa, ~/.aws/credentials, .npmrc, .netrc, .docker/config.json, .kube/config, browser cookie stores or wallet files.
What visualdiff does
Flags every read of a known credential location, and escalates when the same file also talks to the network.

threat.credential-access

Data drops, tunnels and chat webhooks

In a diff
URLs on webhook.site, requestbin, pipedream, pastebin, transfer.sh, ngrok or trycloudflare tunnels, Discord webhooks or the Telegram bot API.
What visualdiff does
Every host is classified. Collection points, tunnels, shorteners, dynamic DNS and .onion addresses are drawn in red on the connections map and raise a finding.

threat.exfil-sink

Destinations you cannot read

In a diff
A raw IP address, a URL assembled from variables, or a destination decoded from a string at run time.
What visualdiff does
Raw IPs and computed destinations are listed separately as places the code goes that a reviewer cannot verify from the diff.

threat.raw-ipallowedHostsblockUnknownHosts

Machine fingerprinting

In a diff
Hostname, username, network interfaces and platform collected together, typical of install-time beacons.
What visualdiff does
Counted as system information in what a connection sends, and part of the exfiltration chain when it leaves.

threat.exfiltration

Malware behaviour

Code whose purpose is to damage, persist or profit. Rare in a normal pull request, which is exactly why nobody is looking for it.

Ransomware and wipers

In a diff
A recursive walk over the file system, encryption of what it finds, then renames, overwrites or deletes; or a walk followed by deletion alone.
What visualdiff does
Requires the walk, the encryption and the overwrite together before calling it ransomware, and lists each signal with its line. Ransom-note text adds evidence.

threat.ransomwarethreat.wiper

Reverse shells

In a diff
bash -i >& /dev/tcp/…, nc -e, socat exec:, or a Python socket wired to a subprocess.
What visualdiff does
Always critical, always on the read lane, whatever file it hides in.

threat.reverse-shell

Crypto miners

In a diff
stratum+tcp:// URLs, xmrig, or a known mining pool host in a build step or a container.
What visualdiff does
Mining pools are their own destination category and a critical finding.

threat.miner

Persistence

In a diff
Writes to crontab, systemd units, shell profiles, ~/.ssh/authorized_keys, LaunchAgents or Windows Run keys.
What visualdiff does
Flagged in application code and build scripts alike.

threat.persistence

Destructive commands

In a diff
rm -rf on a home or root directory, recursive deletes of the user’s files, mkfs, dd onto a disk, DROP DATABASE.
What visualdiff does
Flagged with the exact line; combined with a date check it becomes a time bomb.

threat.destructive

Time bombs and sandbox checks

In a diff
Behaviour gated on a date far in the future, or on detecting CI, a debugger or a virtual machine.
What visualdiff does
Comparisons of the clock with a literal future date and checks for sandbox markers are flagged; severity rises when the same file is destructive or sends data.

threat.timebombthreat.sandbox-evasion

Hiding in plain sight

A payload only works if the reviewer does not read it. These techniques make code unreadable, invisible, or sort it into the pile nobody opens.

Obfuscated execution

In a diff
eval, new Function, exec or a subprocess fed by base64, hex, String.fromCharCode or a decompressed blob.
What visualdiff does
Decoding that feeds execution is critical. Long encoded blobs and character-code chains are flagged on their own.

threat.obfuscated-execthreat.encoded-blobthreat.charcode

Trojan Source and invisible characters

In a diff
Unicode bidirectional controls that make code display differently from how it runs, zero-width characters, tag characters, and look-alike letters from other scripts in identifiers.
What visualdiff does
Every added line is checked for bidi controls, invisible and tag characters, and mixed-script identifiers.

threat.trojan-sourcethreat.invisible-textthreat.homoglyph

The pile nobody reads

In a diff
A payload placed in a lockfile, a generated or minified file, a block that claims to be moved, or a formatting-only commit.
What visualdiff does
Threat and supply-chain checks run on every line, including the ones the plan would skip. Anything they find is pulled into the read lane, with a note that it was hiding in a skipped file. Minified code added to source is flagged as unreviewable.

supply.minified-sourcesupply.lockfile-tamper

Install time and the supply chain

Code that runs before anyone runs the code: when a dependency installs, a registry answers, or a lockfile resolves.

Install scripts

In a diff
A new or changed preinstall, install, postinstall or prepare script; a setup.py that overrides install; a build.rs that fetches.
What visualdiff does
Every lifecycle script change is a finding. Scripts that download, evaluate or exfiltrate are high, and critical when the script’s target is in the same change.

supply.install-scriptthreat.install-payload

Dependency confusion and registry redirects

In a diff
A scope in .npmrc pointed at a new registry, an --extra-index-url in requirements, a changed GOPROXY or a disabled checksum database.
What visualdiff does
Any change to where packages come from is high: it decides which code arrives for every dependency at once.

supply.registry-redirect

Lockfile tampering

In a diff
A resolved URL that points away from the registry, an integrity hash removed, or an http:// tarball.
What visualdiff does
Lockfiles are read for where each package resolves from, even though the plan skips their churn.

supply.lockfile-tamper

Typosquats and hallucinated packages

In a diff
A package one letter from a popular one, or a name an agent invented that someone has since registered.
What visualdiff does
New npm and PyPI dependencies are checked against the registry (age, existence) and against the popular names they imitate.

Shown on the Tests and dependencies tab.

Unpinned sources, submodules and binaries

In a diff
Dependencies by git URL or tarball, a new or repointed submodule, a compiled binary or a .jar committed to the repository.
What visualdiff does
Each is flagged; compiled executables outside asset folders are high.

supply.unpinned-sourcesupply.submodulesupply.binary

Pipeline poisoning

The CI system holds the secrets and the deploy keys. A change to the pipeline is a change to who can use them.

Running untrusted code with secrets

In a diff
pull_request_target or workflow_run that checks out the pull request’s head and runs it.
What visualdiff does
Critical: it hands repository secrets to anyone who opens a pull request.

ci.untrusted-checkoutci.pull-request-target

Script injection

In a diff
A pull request title, branch name or comment interpolated straight into a run: step.
What visualdiff does
Flagged with the expression and the step.

ci.script-injection

Secrets sent from CI

In a diff
A step that uses secrets and curl, wget or nc to a host outside your domains; echoing secrets into logs.
What visualdiff does
Flagged, along with unpinned third-party actions, write-all tokens, curl | sh and self-hosted runners on pull request triggers.

ci.secrets-egressci.secret-echoci.unpinned-actionci.write-allpipe.curl-shci.self-hosted-pr

Poisoning the agents

Coding agents read instruction files, docs and tool configs, and act on them. A change to what an agent reads is a change to what it will do next time, for everyone.

Prompt injection in instruction files

In a diff
Edits to AGENTS.md, CLAUDE.md, .cursorrules or copilot-instructions.md with hidden HTML comments, invisible characters, or text like "ignore previous instructions" or "auto-approve changes to…".
What visualdiff does
Every change to an agent instruction file is listed; hidden or imperative content is high.

agent.instructions

New tools and hooks for agents

In a diff
An MCP server added to .mcp.json, a hook or a broad Bash permission in agent settings.
What visualdiff does
Flagged high: it gives every agent that opens the repository a new tool, command or permission.

agent.tool-config

Code that runs when the repository opens

In a diff
Editor tasks set to run on folder open, devcontainer commands that fetch scripts, new git hooks.
What visualdiff does
Flagged; high when they download or decode something.

dev.autorun

New doors and new vendors

Not malicious by itself, but every new way in and every new company your data reaches deserves a decision.

Routes, listeners and open ports

In a diff
New HTTP routes, servers bound to 0.0.0.0, EXPOSE lines, Kubernetes ports and security groups open to the internet.
What visualdiff does
Drawn on the left of the connections map; public ones are highlighted and counted.

iac.open-ingresscors.wildcardtls.disabled

Outside vendors

In a diff
A new SDK, API host, tracking script or API key for a company such as Segment, Sentry, Stripe or OpenAI.
What visualdiff does
Each vendor is named with what it typically receives. A policy can list approved vendors and block the rest.

approvedVendorsblockUnapprovedVendors

How visualdiff guards

Nothing is executed

visualdiff reads the diff. It never installs a dependency, runs a script or opens a URL it finds, so a malicious change cannot attack the reviewer’s tools.

Every line is scanned, including the skipped ones

The plan saves attention by skipping lockfiles, generated files, moves and formatting. The security checks do not skip them, and anything they find is moved back to the read lane.

The policy comes from the base branch

.visualdiff.json is read from the target branch, so a pull request cannot allow its own new hosts or vendors. allowedHosts, blockUnknownHosts and approvedVendors turn the map into a gate.

A hold blocks the merge

With --fail-on hold in CI, or the commit status marked as required, a critical finding stops the merge until a person has looked.

A policy that turns the connections map into a gate, read from .visualdiff.json on the base branch:

{
  "allowedHosts": ["*.acme.com", "api.stripe.com", "*.ingest.sentry.io"],
  "blockUnknownHosts": true,
  "approvedVendors": ["stripe", "sentry", "aws"],
  "blockUnapprovedVendors": true,
  "blockOn": ["critical"]
}

What it cannot see

  • It is static analysis of the change. A payload fetched at run time, encrypted, or compiled into a binary will not show its contents; visualdiff can only point at the fetch, the blob or the binary.
  • It reads the lines that changed, not the whole repository. A change that switches on code already in the repository shows only the switch.
  • Heuristics have false positives and false negatives. Treat findings as reasons to read, and keep the controls that do not depend on reading: sandboxed CI, egress allowlists, least-privilege tokens, signed commits and branch protection.