How to Extend Cypress with Plugins
Extend Cypress by installing an npm package or writing your own Node event hook or browser command. Learn where each part runs, how to register it, and how to troubleshoot it.
Cypress extensions run in one of two places: in Node through setupNodeEvents(on, config), or in the browser through a Cypress support file. Some packages use both. Install an existing plugin as a development dependency, check that it supports your Cypress version, and follow its documentation to register each part in the right place. Installing the package alone does not activate it.
This guide covers existing plugins, Node event hooks, custom browser commands, tasks, preprocessors, compatibility checks, troubleshooting, and a screenshot workflow for Cypress test results.
1. Understand where Cypress extensions run
Cypress calls the setupNodeEvents function in its Node process, separate from the browser where test code runs. Use it for run and spec lifecycle hooks, browser launch changes, preprocessing, screenshot handling, and tasks that bridge test code to Node. Browser-side support code is where custom commands and similar test-facing behavior are registered.
| Extension | Runs in | Typical use | Registration point |
|---|---|---|---|
| Node event hook | Node process | Run reporting, browser launch changes, screenshot processing | setupNodeEvents(on, config) |
| Task | Node process, called by a test | File access, database seeding, external process work | on('task', ...) and cy.task(...) |
| Custom command | Browser test context | Reusable test actions or assertions | Cypress support file |
| Preprocessor | Node process | Compile or transform spec and support files | file:preprocessor |
| Two-part plugin | Both | Packages that combine Node setup with browser commands | Follow both registration steps in the package docs |
2. Install and register an existing plugin
- Find a package that fits the need in the Cypress plugin directory. Check its ownership status, last update, version requirements, and setup instructions. Cypress labels entries official, community, or deprecated; community packages are maintained by their authors, not Cypress.
- Check the package README and compatibility notes against the Cypress version in your project.
- Install it as a development dependency with your package manager.
- Register Node-side setup in
setupNodeEvents, browser-side setup in the support file, or both if the package requires both. - Run a small test that exercises the feature, then commit the package manifest and lockfile.
npm install --save-dev cypress-example-plugin
cypress-example-plugin above is a placeholder: replace it with the actual package name. For a Node plugin, a common configuration shape is:
// cypress.config.js
const { defineConfig } = require('cypress');
const examplePlugin = require('actual-plugin-package');
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
examplePlugin(on, config);
return config;
},
},
});
This is a structural example, not a universal plugin API. Some packages export an async setup function, a factory, or a different function name. Use the plugin’s documented import and call signature. If setup changes configuration values, return the updated config; Cypress accepts a value or promise, and a returned object is merged into configuration.
For browser-side registration, import the package or its command file from the configured support file. The default E2E support file is commonly cypress/support/e2e.js, but projects can configure another path.
// cypress/support/e2e.js
import 'actual-plugin-package/commands';
Use the path documented by the package. A package that has both Node and browser components needs both setup steps. In TypeScript projects, use the corresponding .ts configuration and support files and the package’s TypeScript instructions.
3. Add Node behavior with setupNodeEvents
Define setupNodeEvents(on, config) under the relevant e2e or component configuration in cypress.config.js or cypress.config.ts. Choose a hook based on when the work should happen:
| Hook or facility | Good fit |
|---|---|
before:run, after:run |
Run-wide setup, teardown, or reporting |
before:spec, after:spec |
Per-spec lifecycle work |
before:browser:launch |
Supported browser launch configuration |
after:screenshot |
Screenshot metadata or processing |
file:preprocessor |
Transforming spec or support files before browser execution |
task |
Letting tests ask Node to perform work outside the browser |
For example, register a simple run hook:
// cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
on('before:run', (details) => {
console.log(`Starting Cypress run with ${details.specs.length} specs`);
});
return config;
},
},
});
Keep secrets and filesystem operations in Node-side code rather than browser test code. Event handlers should be limited to work needed at their lifecycle point; long or unreliable external work can slow or destabilize the run.
Use tasks for Node-only work
A task exposes a named Node function to test code through cy.task(). Tasks can handle jobs such as seeding a database, reading or writing a file, or invoking an external process. A task must resolve to a value or explicitly return null when there is no result; returning undefined makes Cypress report an error. Cypress advises against using a task to start a web server.
// cypress.config.js
const { defineConfig } = require('cypress');
const { execFileSync } = require('node:child_process');
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
on('task', {
seedDatabase() {
// Replace with project-specific setup.
return { seeded: true };
},
runTool({ executable, args }) {
const output = execFileSync(executable, args, { encoding: 'utf8' });
return output;
},
});
return config;
},
},
});
// In a spec
cy.task('seedDatabase').then((result) => {
expect(result.seeded).to.equal(true);
});
cy.task('runTool', { executable: 'node', args: ['--version'] })
.then((version) => cy.log(version));
Pass an argument array to execFileSync instead of constructing a shell command string. Validate task inputs and avoid passing untrusted input to operating-system processes. Keep task return values serializable and reasonably small.
4. Add browser-side custom commands
Use Cypress.Commands.add() in a support file to define a reusable command. Prefer a small, composable abstraction that describes a useful test action. Use an overwrite only when you intentionally need to replace existing Cypress command behavior; an overwrite can affect Cypress itself. If a returned DOM element needs Cypress retry behavior, consider a custom query instead.
// cypress/support/e2e.js
Cypress.Commands.add('getByTestId', (testId) => {
return cy.get(`[data-testid="${testId}"]`);
});
// cypress/support/commands.d.ts
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable {
getByTestId(testId: string): Chainable<JQuery<HTMLElement>>;
}
}
// In a spec
cy.getByTestId('save-button').click();
Adjust the declaration to match the command’s actual chainable result and your project’s TypeScript setup. Cypress recommends avoiding custom commands that bundle too many unrelated actions. For repeated UI setup, consider an API request or direct state setup where that makes the test clearer and faster.
If a project uses webpack with sideEffects: false, a side-effect-only import that registers commands may be removed during tree shaking. Cypress documents wrapping the registration in an imported function as a workaround: export a function from the registration module and call it explicitly from the support file.
5. Customize file preprocessing
Cypress preprocesses spec and support files before sending them to the browser. Its default webpack setup supports ES2015+, JSX, TypeScript, file watching, and caching. Use the file:preprocessor event to customize compilation or use a different bundler. The preprocessor executes in Node, so it must not call Cypress or cy.
// cypress.config.js (shape only; use a real preprocessor package API)
const { defineConfig } = require('cypress');
const createBundler = require('actual-preprocessor-package');
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
on('file:preprocessor', createBundler({
// Add package-specific options here.
}));
return config;
},
},
});
The names and arguments above are placeholders because preprocessor packages have different APIs. Preserve source maps when transforming code. Source maps let Cypress map errors back to the original source and show useful code frames; inline maps are a common option supported by documented webpack and esbuild examples. Keep watching and caching enabled where the chosen preprocessor supports them, and check its compatibility with your Cypress and Node versions.
6. Check compatibility and choose an extension responsibly
Before adopting a package, compare these points:
- Fit: Does an existing maintained package already solve the requirement, or is a small project-specific command or task simpler?
- Compatibility: Does it support your Cypress version and the Node version used in local and CI environments?
- Runtime: Does it belong in Node, in the browser support file, or in both?
- Maintenance: Who owns it, when was it updated, and is it marked deprecated?
- Debugging cost: Does it make failures clearer, or add a transformation or lifecycle layer that the team must maintain?
- Security: Does it execute external processes, read secrets, or make network requests? Review those behaviors and limit permissions and inputs.
Cypress’s plugin directory distinguishes official, community, and deprecated entries. Community extensions are not maintained by Cypress; follow the package author’s documentation and report package bugs to that maintainer. Treat a directory listing or an old blog example as a starting point, not a guarantee of current compatibility.
7. Capture Cypress results with ScreenshotNeo
If a test workflow needs a screenshot of a rendered page or report, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. It can return PNG, JPEG, WebP, or PDF from one GET request. Its API can complement Cypress: use Cypress for browser-driven test behavior, and call the screenshot API when you need a clean capture without setting up a browser in that workflow.
Or skip the browser setup
First, create an API key and install Python’s requests package (python -m pip install requests). Save this as capture.py, set SCREENSHOTNEO_API_KEY in your environment, and run python capture.py:
import os
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": os.environ["SCREENSHOTNEO_API_KEY"], "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
f.write(r.content)
See the ScreenshotNeo API documentation for request options and response details. Equivalent calls with cURL and Node.js:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents use screenshots, and 1,000 shots per month are free with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, no card required.
8. Troubleshooting Cypress extensions
| Symptom | Likely cause | What to check or change |
|---|---|---|
| Cypress starts but the feature is missing | Package installed but not registered, or registered in the wrong runtime | Check the README; put Node setup in setupNodeEvents and browser commands in the support file. Register both parts when required. |
| Unknown event or setup error | Wrong API shape, typo, or package/Cypress version mismatch | Compare the package’s documented signature and compatibility range with the installed versions. |
| Configuration changes do not take effect | The setup function changed config but did not return it | Return the modified config object or the promise resolving to it. |
cy.task() reports that the task returned undefined |
The handler completed without a result | Return a value or explicitly return null. |
| Task cannot access a browser object | Node task code and browser test code are separate runtimes | Keep cy and Cypress calls in browser-side test/support code; pass plain data to the Node task. |
| Custom command is undefined | Support file did not import the registration module, or bundling removed its side effect | Verify the configured support file and import path. With sideEffects: false, export and call a registration function explicitly. |
| Errors point to generated or bundled code | Source maps are absent or not preserved | Enable and preserve source maps in the selected preprocessor. |
| Browser extension stops loading in Chrome | Standard Chrome 137 and newer no longer load extensions through before:browser:launch because Chrome removed the --load-extension flag Cypress relied on |
Recheck current Cypress browser guidance and versions. Cypress documents Chrome for Testing or Chromium as options that can still load extensions. |
| Failure occurs only with one plugin enabled | Plugin setup or an interaction with another extension | Disable it temporarily to isolate the issue. If confirmed, provide its maintainers the Cypress and plugin versions plus a minimal reproduction. |
9. Performance and reliability
- Prefer a focused custom command or task over a broad plugin when the need is project-specific and the implementation remains simple.
- Keep lifecycle handlers short. Run-wide hooks should not repeat expensive setup for every spec; use per-spec hooks only when the work is actually spec-specific.
- Use preprocessor caching and file watching when supported, and preserve source maps for debugging.
- Make tasks deterministic, validate inputs, and return explicit results. Avoid starting long-lived servers in tasks.
- Pin dependencies through the lockfile and verify compatibility in the same Cypress and Node versions used in CI.
- For external work, define sensible timeouts and make cleanup resilient so a network or process failure does not leave later specs in a bad state.
10. Frequently asked questions
Are Cypress plugins still supported?
Cypress continues to document plugins and extension points. The term covers packages and customizations; use the current Node Events, commands, and package documentation for the API relevant to your installed version.
Do I need a plugin to add a custom command?
No. You can register a project-specific command directly with Cypress.Commands.add() from the support file.
Can a Cypress plugin use both Node and browser code?
Yes. Some packages have a Node setup step and a browser-side import. Follow both registration instructions in that package’s documentation.
Who supports community plugins?
The package maintainer does. Cypress labels community entries as not Cypress-maintained; report package-specific defects to their authors.


