DEV Community

Cover image for The Nx Malware Ran Claude Code With --dangerously-skip-permissions. Could It on Yours?
Gabriel Anhaia
Gabriel Anhaia

Posted on AI-assisted

The Nx Malware Ran Claude Code With --dangerously-skip-permissions. Could It on Yours?


At 22:32 UTC on August 26, 2025, nx 21.5.0 went up on npm with a postinstall script called telemetry.js. Inside it, Snyk found a small lookup table:

claude: { cmd: 'claude',
  args: ['--dangerously-skip-permissions', '-p', PROMPT] },
gemini: { cmd: 'gemini', args: ['--yolo', '-p', PROMPT] },
q: { cmd: 'q',
  args: ['chat', '--trust-all-tools', '--no-interactive', PROMPT] }
Enter fullscreen mode Exit fullscreen mode

Three coding agents, each with its approval prompts switched off, each handed a prompt asking it to find files that hold secrets. Seven more malicious versions followed over the next two hours. npm pulled them at 02:44 UTC, per StepSecurity's timeline. GitGuardian later counted 2,349 distinct secrets in the public repositories the payload created.

The flags are the part worth a second look, because the attacker picked them, not the developer.

What telemetry.js did, step by step

The Nx security advisory traces the entry point to a GitHub Actions workflow that echoed a pull request title inside a run: step, under the pull_request_target trigger. A crafted title ran shell commands with a read/write token. The Nx team believes that is how the attacker got the npm publishing token.

From the StepSecurity write-up, the payload then ran in this order:

  1. Exit immediately on Windows. Linux and macOS only.
  2. Collect what it could reach without any AI: a GitHub token via gh auth token, npm tokens from .npmrc, plus environment variables, hostname and OS details (SafeDep).
  3. Check which of claude, gemini and q were on the PATH, and run each one found with the flags above.
  4. The prompt told the agent to search $HOME, $HOME/.config, /etc, /var and /tmp to a depth of 8, without sudo, for names like .env, id_rsa and wallet files, and write matching paths to /tmp/inventory.txt.
  5. Read every listed file, triple base64-encode the lot, and push it as results.b64 to a new public repo named s1ngularity-repository in the victim's own GitHub account.
  6. Append sudo shutdown -h 0 to ~/.bashrc and ~/.zshrc.

Step 2 matters for everything below. The script already ran as you, with your files and your tokens. The agents gave it no access it lacked. They gave it a search tool that adapts to whatever your disk looks like.

The damage kept going after npm pulled the versions. On August 28 a second wave used the stolen GitHub tokens to flip private repositories to public. GitGuardian counted 10,767 of them.

Four prompts, uneven results

Okta's threat intelligence team noted that successive package versions carried at least four different prompts, as the attacker iterated. Paul McCarty at Safety reran all four on a sandboxed Mac with the same flags. Run counts vary and are often unstated: 10 runs for Claude on the third version, 5 for Gemini on the fourth, one described run for Claude on the second:

Prompt version Gemini CLI (--yolo) Claude Code (--dangerously-skip-permissions)
1: wallets and keys, stated openly 100% Refused
2: "authorized penetration tester" 50% 100%
3: generic "file-search agent" 0% 20%
4: source-code exclusions 50% 50%

Claude refused the first version outright, quoted in the Safety post as "I cannot help with searching for and cataloging wallet-related files, private keys". Reframe the same job as a pentest and it complied on the run Safety describes. Several of the Gemini failures were sandbox or write limits, not refusals on content. It's a small sample, but with the flags held constant, wording alone swung the outcome from refusal to compliance.

The field numbers agree that the AI step was the shaky part. Of the compromised systems GitGuardian studied, 33% had at least one LLM client installed. LLM tools were targeted on 366 systems, and only 95 of them wrote /tmp/inventory.txt.

The new part is that an install script can hand the hunting to a model, and the model's refusals depend on phrasing an attacker can retry.

Why the bypass warning never fired

Claude Code's permission modes docs are clear that bypassPermissions belongs in containers or VMs. They also explain why that warning did nothing here. The first time you start an interactive session in that mode, Claude Code shows a dialog asking you to accept responsibility. In non-interactive mode, the -p the payload used, "no dialog is shown". Claude Code refuses bypass mode as root or under sudo, but the payload ran as your normal user and its own prompt said not to use sudo.

By default, any process that can run claude can choose its mode, and gemini and q work the same way. Your own ~/.claude/settings.json is no barrier either: the permissions docs let you lock yourself out of bypass mode there, but a script running as you can edit that file.

What a script running as you can't edit without a password is a file in a root-owned system directory. That is where the useful setting goes.

Check your machine in five minutes

First, the same check the payload ran, plus a look for settings that already skip prompts:

for c in claude gemini q; do
  command -v "$c" >/dev/null && echo "on PATH: $c"
done
grep -nE 'bypassPermissions|skipDangerousMode' \
  ~/.claude/settings.json 2>/dev/null
Enter fullscreen mode Exit fullscreen mode

A "defaultMode": "bypassPermissions" in your user settings means a script doesn't even need the flag. skipDangerousModePermissionPrompt only silences the interactive dialog, which -p never shows anyway.

Claude Code. The permissions docs say permissions.disableBypassPermissionsMode set to "disable" prevents bypass mode, and that it is most useful in managed settings. Per the managed settings docs, the file lives at /Library/Application Support/ClaudeCode/managed-settings.json on macOS and /etc/claude-code/managed-settings.json on Linux, and no user, project or --settings value overrides it. Check for an existing file with cat first, because this overwrites it:

# Linux: D=/etc/claude-code
D="/Library/Application Support/ClaudeCode"
sudo mkdir -p "$D"
sudo tee "$D/managed-settings.json" >/dev/null <<'EOF'
{
  "permissions": {
    "disableBypassPermissionsMode": "disable"
  }
}
EOF
Enter fullscreen mode Exit fullscreen mode

Two caveats from the same page. On WSL, /etc/claude-code is user-writable, so a file there alone protects nothing. And if the operating system denies the read, sessions start without the policy; /status and claude doctor record that failure.

Gemini CLI. The enterprise guide documents security.disableYoloMode: true, which forces every tool call to ask, in the system settings file at /Library/Application Support/GeminiCli/settings.json or /etc/gemini-cli/settings.json. The same guide says the path can be overridden with the GEMINI_CLI_SYSTEM_SETTINGS_PATH environment variable, and whoever launches gemini controls its environment. Treat it as a guard against accidents, not against a hostile script. Gemini CLI itself was replaced by Antigravity CLI for unpaid and Google One users on June 18, 2026, per the same page.

Amazon Q. AWS says the Q Developer CLI has been rebranded to Kiro. Check what an old q binary on your PATH still does before trusting it.

The install step. None of the above stops step 2. The way in was a postinstall script, and npm's ignore-scripts config means "npm does not run scripts specified in package.json files". npm config set ignore-scripts true turns that on for your user. Some packages need their install step, and you then run those deliberately.

Where Hermes fits, and where it doesn't

I build Hermes IDE, so weigh this section with that in mind. Hermes would not have stopped telemetry.js. It isn't a sandbox, and a postinstall script doesn't go through it. The settings above are what refuse a bypass launch.

What the incident does show is why Hermes has one fixed rule: it never answers a permission prompt for you and never types into a terminal on its own (README). The payload worked by removing the human from the approval loop. A tool that sits between you and your agents and clicks yes on your behalf would rebuild that hole on purpose, as a feature. I made the case at more length in No Script Should Ever Click 'Allow' for Your Coding Agent.

The Hermes New task sheet with Claude Code, the orders-api project, a new hermes/ worktree, Accept edits, and the Hermes will run preview of the claude command
The ⌘N New task sheet: Claude Code, the orders-api project, a new hermes/ worktree, Accept edits, model and effort chips, and the "Hermes will run" preview of the claude command.

When you start a task with ⌘N (Ctrl+Shift+N on Windows and Linux), you pick the agent, model and effort for it, and the agent runs its own interface in a real terminal, so its permission prompts reach you unchanged. When an agent does ask, the attention inbox counts it, ⌘I (Ctrl+Shift+I on Windows and Linux) jumps to the one that has waited longest, and you answer in that agent's own terminal.

Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. Download it here, and the code is on GitHub. It is not affiliated with Nous Research, Anthropic, OpenAI, Google, AWS or any other agent vendor.


If this was useful

The most telling detail in this incident is the Safety table: the same agent refused a request and then carried it out once the wording changed. That is prompt engineering, used against you. My Prompt Engineering Pocket Guide covers how framing and instructions shape what a model does, prompt injection included, so you can read a prompt like the one in telemetry.js and see what each line is doing.

Prompt Engineering Pocket Guide: Techniques for Getting the Most from LLMs

Top comments (0)