The Nx Malware Ran Claude Code With --dangerously-skip-permissions. Could It on Yours?
- Book: Prompt Engineering Pocket Guide
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
---
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] }
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
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
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.

Say which projects may depend on which
Tag your projects by layer and say which layers may depend on which.
vx lock refuses a dependency that breaks a rule. Here UI may not
depend on a utility, and packages/c (UI) depends on packages/a:
!diagram
// .vx/workspace.ts
import { defineWorkspace } from '@vzn/vx/config'
export default defineWorkspace({
run: {
rules: {
dependencies: [{ from: 'tag:type:ui', deny: ['tag:type:util'] }],
},
},
})
$ vx lock
✗ vx lock lock not written
1 dependency breaks run.rules.dependencies
packages/c depends on packages/a, which run.rules.dependencies[0] (from 'tag:type:ui') denies
fix drop the dependency, or change the project's tags or the rule
$ echo $?
1
The edge here is a task edge (c#build depends on a#build), not a
package.json dependency. Both count.
<small>Demo workspace · vx 69617eb · 2026-10-23</small>
Tags and rules
A project names its tags in its own config:
// packages/button/vx.config.ts
import { defineProject } from '@vzn/vx/config'
export default defineProject({
tags: ['type:ui'],
tasks: { build: { exec: { command: 'tsc -b' } } },
})
The workspace says what each tag may depend on. A selector is
tag:<glob> or a project-name glob:
// .vx/workspace.ts
import { defineWorkspace } from '@vzn/vx/config'
export default defineWorkspace({
run: {
rules: {
dependencies: [
{ from: 'tag:type:app', allow: ['tag:type:feature', 'tag:type:ui'] },
{ from: 'tag:type:ui', deny: ['tag:type:feature', 'tag:type:app'] },
],
},
},
})
A dependency must match one of allow (if given) and none of deny.
A deny beats any allow. A project no rule selects is free.
vx lock checks them, runs don't
vx lock checks each project's direct dependencies: package.json and
cross-project dependsOn. It names every broken edge and writes
nothing. Fix the edge and the lock is written:
$ vx lock
✓ vx lock 6 project configs → .vx/projects.lock.json
$ vx lock --check
✓ vx lock --check up to date (6 projects)
vx lock --check in CI fails the same way, so a pull request with a
forbidden dependency goes red. Runs read the lock, which already passed
the rules, so they pay nothing (vx lock).
From Nx
nx() from @vzn/vx-migrate reads the depConstraints of
@nx/enforce-module-boundaries from your ESLint config and fills
run.rules.dependencies. It reads the file without running it, so
ESLint never loads on a vx run.
// eslint.config.mjs (Nx)
depConstraints: [
{ sourceTag: 'type:app', onlyDependOnLibsWithTags: ['type:feature', 'type:ui'] },
{ sourceTag: 'type:ui', notDependOnLibsWithTags: ['type:feature'] },
]
becomes:
dependencies: [
{ from: 'tag:type:app', allow: ['tag:type:feature', 'tag:type:ui'] },
{ from: 'tag:type:ui', deny: ['tag:type:feature'] },
]
One difference: Nx's
lint rule
checks source imports. vx checks declared dependencies. allSourceTags,
/regex/ tags and external-import bans are left out. The
vx-migrate README
lists them. The schema is in the
configuration reference. More at
vznjs.github.io.
---
Originally published on the vx blog. vx is open-source (MIT) task orchestration with a build cache: vznjs.github.io · GitHub.
<sub>_Written with AI assistance._</sub>
Mentions
CVE-2026-119282: CVE-2026-119282: OS Command Injection via Plugin Options in Nx (@nx/maven and @nx/gradle) CVE-2026-119282: OS Command Injection via Plugin Options in Nx (@nx/maven and @nx/gradle) > Vulnerability ID: CVE-2026-119282 > CVSS Score: 7.3 > Published: 2026-10-30 An OS command injection and argument injection vulnerability exists in the Nx build system and its official Maven (@nx/maven) and Gradle (@nx/gradle) plugins. When constructing the project dependency graph, the plugins invoke underlying Java build wrappers via subshells while interpolating unsanitized plugin options directly from nx.json into the command arguments. An attacker who supplies a crafted nx.json file via a repository or pull request can execute arbitrary shell commands with the privileges of the user or CI/CD runner invoking any Nx command that computes the project graph. TL;DR Nx versions prior to 22.7.9 and 23.1.2 pass unvalidated configuration options from nx.json into child process executions with shell: true in @nx/maven and @nx/gradle. Running Nx commands in an untrusted repository or pull request allows arbitrary OS command execution. --- ⚠️ Exploit Status: POC Technical Details - Vulnerability Class: OS Command Injection (CWE-78) / Argument Injection (CWE-88) - CVSS v4.0 Score: 7.3 HIGH (CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) - Attack Vector: Local via untrusted nx.json in repository or pull request - Affected Components: nx, @nx/maven, @nx/gradle (>= 21.0.0, < 22.7.9 and >= 23.0.0, < 23.1.2) - Patched Versions: 22.7.9 and 23.1.2 - EPSS Score: 0.14% (3.38th percentile) - CISA KEV Status: Not Listed Affected Systems - nx - @nx/maven - @nx/gradle - nx: >= 21.0.0, < 22.7.9 (Fixed in: 22.7.9) - nx: >= 23.0.0, < 23.1.2 (Fixed in: 23.1.2) - @nx/maven: >= 21.0.0, < 22.7.9 (Fixed in: 22.7.9) - @nx/maven: >= 23.0.0, < 23.1.2 (Fixed in: 23.1.2) - @nx/gradle: >= 21.0.0, < 22.7.9 (Fixed in: 22.7.9) - @nx/gradle: >= 23.0.0, < 23.1.2 (Fixed in: 23.1.2) Code Analysis Commit: 02c5d59 Introduce safeSpawn and safeExecFileSync utilities and replace shell: true invocations in @nx/maven and @nx/gradle. Commit: 60196b7 Backport safeSpawn and plugin analyzer security fixes for CVE-2026-119282. Commit: c227f47 Release commit containing fixes for @nx/maven and @nx/gradle command injection. Mitigation Strategies - Upgrade both the core nx package and any installed @nx/maven or @nx/gradle plugins to matched release versions 22.7.9 or 23.1.2. - Enforce strict code review requirements on nx.json using repository CODEOWNERS rules, treating it with the same sensitivity as CI workflow definitions and build scripts. - Isolate CI/CD execution on untrusted pull requests so that nx commands do not run with access to production secrets or write permissions before human review. Remediation Steps: 1. Identify whether @nx/maven or @nx/gradle is installed in package.json or configured in nx.json. 2. Run 'nx migrate 22.7.9' (for 22.x workspaces) or 'nx migrate 23.1.2' (for 23.x workspaces) to update package.json dependencies. 3. Install the updated packages using your package manager (npm install, yarn, or pnpm install). 4. Verify that node_modules/nx, node_modules/@nx/devkit, node_modules/@nx/maven, and node_modules/@nx/gradle all resolve to version 22.7.9 or 23.1.2+. References - GitHub Security Advisory GHSA-qm8w-vx34-5j9j - NVD - CVE-2026-119282 Detail - CVE.org Record: CVE-2026-119282 - CVEListV5 Official JSON Record for CVE-2026-119282 - GitHub Pull Request #36626 - Fix Command Injection in Maven and Gradle Plugins - Fix Commit 02c5d59e8dbbbe30963c0181c6a22cd8c1190a25 - Fix Commit 60196b7cfcffcada35b6b90e53bf1a78d049b548 - Fix Commit c227f4780b054c512965c73aa480a48f9f781409 - Nx Release 22.7.9 - Nx Release 23.1.2 --- Read the full report for CVE-2026-119282 on our website for more details including interactive diagrams and full exploit analysis.
The Nx Malware Ran Claude Code With --dangerously-skip-permissions. Could It on Yours? - Book: Prompt Engineering Pocket Guide - Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- 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] } 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 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 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. 
⚡ [Top 5 AI Repos This Week] nx: The Free Tool Taking Over GitHub! #shorts Discover nrwl/nx: The Monorepo Platform that amplifies both developers and AI agents. Nx optimizes your builds, scales your CI, and fixes failed PRs automatically. Ship in half the time. 💬 Comment "REPO" below for the direct link, or grab it from the links below: 👉 Interactive Star History & Live Analysis: githubrepo.cloud ⭐ Embed Live Verified Badges in your README: githubrepo.cloud 🔗 Direct GitHub Repository: nrwl/nx 📊 Repository Specs: ⭐ Stars: 29,393 🍴 Forks: 3,008 💻 Language: TypeScript 📜 License: MIT #shorts #angular #build #buildsystem #buildtool #musicproducer #audioengineer #stems #musicproduction
Say which projects may depend on which Tag your projects by layer and say which layers may depend on which. vx lock refuses a dependency that breaks a rule. Here UI may not depend on a utility, and packages/c (UI) depends on packages/a: !diagram // .vx/workspace.ts import { defineWorkspace } from '@vzn/vx/config' export default defineWorkspace({ run: { rules: { dependencies: [{ from: 'tag:type:ui', deny: ['tag:type:util'] }], }, }, }) $ vx lock ✗ vx lock lock not written 1 dependency breaks run.rules.dependencies packages/c depends on packages/a, which run.rules.dependencies[0] (from 'tag:type:ui') denies fix drop the dependency, or change the project's tags or the rule $ echo $? 1 The edge here is a task edge (c#build depends on a#build), not a package.json dependency. Both count. <small>Demo workspace · vx 69617eb · 2026-10-23</small> Tags and rules A project names its tags in its own config: // packages/button/vx.config.ts import { defineProject } from '@vzn/vx/config' export default defineProject({ tags: ['type:ui'], tasks: { build: { exec: { command: 'tsc -b' } } }, }) The workspace says what each tag may depend on. A selector is tag:<glob> or a project-name glob: // .vx/workspace.ts import { defineWorkspace } from '@vzn/vx/config' export default defineWorkspace({ run: { rules: { dependencies: [ { from: 'tag:type:app', allow: ['tag:type:feature', 'tag:type:ui'] }, { from: 'tag:type:ui', deny: ['tag:type:feature', 'tag:type:app'] }, ], }, }, }) A dependency must match one of allow (if given) and none of deny. A deny beats any allow. A project no rule selects is free. vx lock checks them, runs don't vx lock checks each project's direct dependencies: package.json and cross-project dependsOn. It names every broken edge and writes nothing. Fix the edge and the lock is written: $ vx lock ✓ vx lock 6 project configs → .vx/projects.lock.json $ vx lock --check ✓ vx lock --check up to date (6 projects) vx lock --check in CI fails the same way, so a pull request with a forbidden dependency goes red. Runs read the lock, which already passed the rules, so they pay nothing (vx lock). From Nx nx() from @vzn/vx-migrate reads the depConstraints of @nx/enforce-module-boundaries from your ESLint config and fills run.rules.dependencies. It reads the file without running it, so ESLint never loads on a vx run. // eslint.config.mjs (Nx) depConstraints: [ { sourceTag: 'type:app', onlyDependOnLibsWithTags: ['type:feature', 'type:ui'] }, { sourceTag: 'type:ui', notDependOnLibsWithTags: ['type:feature'] }, ] becomes: dependencies: [ { from: 'tag:type:app', allow: ['tag:type:feature', 'tag:type:ui'] }, { from: 'tag:type:ui', deny: ['tag:type:feature'] }, ] One difference: Nx's lint rule checks source imports. vx checks declared dependencies. allSourceTags, /regex/ tags and external-import bans are left out. The vx-migrate README lists them. The schema is in the configuration reference. More at vznjs.github.io. --- Originally published on the vx blog. vx is open-source (MIT) task orchestration with a build cache: vznjs.github.io · GitHub. <sub>_Written with AI assistance._</sub>
@juristr @victorsavkin @brandontroberts Sure! It has been 6 years(!!) at this point, but if I remember correctly there was a decision back around NX 11 that removed support for configuration composition a la angular cli, due to issues relating to task output caching nx#2839
@victorsavkin any news about the conformance model going free/open source as well? nx#34854