Your Puppeteer scripts are getting blocked. Not because your code is bad—but because your mouse movements are too perfect.
Real humans don't move cursors in straight lines at constant velocity. They overshoot targets, hesitate, and follow curved paths. Anti-bot systems know this, and they're watching.
Ghost Cursor fixes this problem. It generates human-like mouse movements using Bezier curves and Fitts's Law, making your browser automation appear natural to behavioral analysis systems.
In this guide, you'll learn everything about Ghost Cursor—from installation to advanced techniques that help you avoid detection.
What is Ghost Cursor?
Ghost Cursor is a Node.js library that generates realistic, human-like mouse movement data for browser automation tools like Puppeteer.
Instead of jumping instantly from point A to point B (like standard automation tools), Ghost Cursor calculates curved paths with natural acceleration and deceleration patterns.
In practice, Ghost Cursor can:
- Generate smooth Bezier curve paths between any two coordinates
- Overshoot targets and self-correct—mimicking real human behavior
- Vary movement speed based on distance and target size
- Click at random points within elements (not dead center)
- Work as a standalone path generator or integrate directly with Puppeteer
Ghost Cursor has over 62,000 weekly downloads on npm and 1,400+ GitHub stars.
It's popular among scraping engineers, automation developers, and anyone dealing with anti-bot systems that analyze behavioral patterns.
The library was created by Xetera and uses mathematical models (Bezier curves and Fitts's Law) that mirror actual human motor control.
Why use Ghost Cursor?
Standard browser automation creates obvious bot signals that detection systems catch instantly.
When Puppeteer's page.click() executes, the cursor teleports to the exact center of an element and clicks with zero hesitation. No human moves a mouse like that.
Here's what anti-bot systems detect:
| Behavior | Human | Standard Automation | Ghost Cursor |
|---|---|---|---|
| Mouse path | Curved, irregular | Straight line or teleport | Bezier curves |
| Click position | Random within target | Exact center | Random within element |
| Movement speed | Variable, distance-aware | Constant or instant | Fitts's Law based |
| Overshoot | Common on distant targets | Never | Built-in |
| Hesitation | Natural pauses | None | Configurable delays |
Ghost Cursor addresses all of these signals.
Choose Ghost Cursor when you need:
- To bypass behavioral analysis systems like DataDome or PerimeterX
- Natural-looking interactions on sites that track
mousemoveevents - More realistic form filling and button clicking
- Reduced detection rates on protected websites
That said, Ghost Cursor isn't magic. It handles one layer of bot detection—mouse behavior.
Sites also fingerprint browsers, analyze TLS handshakes, and track request patterns. You'll still need stealth plugins, proper proxies, and request management for comprehensive protection.
How to install Ghost Cursor
Before installing, make sure you have:
- Node.js 14 or higher
- npm or yarn
- Puppeteer (if using browser integration)
Installation with npm
# Create a new project directory
mkdir ghost-cursor-project
cd ghost-cursor-project
npm init -y
# Install Ghost Cursor and Puppeteer
npm install ghost-cursor puppeteer
Installation with yarn
yarn add ghost-cursor puppeteer
Verify installation
Create a test file to confirm everything works:
// test.js
const { path } = require('ghost-cursor');
const from = { x: 100, y: 100 };
const to = { x: 500, y: 400 };
const route = path(from, to);
console.log('Generated', route.length, 'points');
console.log('First point:', route[0]);
console.log('Last point:', route[route.length - 1]);
Run it:
node test.js
Expected output:
Generated 47 points
First point: { x: 100, y: 100 }
Last point: { x: 500, y: 400 }
The exact point count varies because Ghost Cursor adds randomness to path generation.
Basic concepts
Before diving into code, understand how Ghost Cursor creates human-like movements.
Bezier curves
Ghost Cursor uses cubic Bezier curves to generate paths between two points.
Unlike straight lines, Bezier curves can arc and bend based on control points. The library picks random control points above and below the direct path to create natural-looking curves.
Each path looks different because the control points are randomized—just like no two human mouse movements are identical.
Fitts's Law
Fitts's Law is a predictive model of human movement from 1954.
It states that the time to reach a target depends on the distance to the target and the target's size. Closer, larger targets are faster to reach.
Ghost Cursor uses this principle to calculate realistic movement speeds. Moving to a small button far away takes longer than moving to a large button nearby.
Overshoot and correction
When targets are far from the cursor's starting position, humans often overshoot slightly, then correct back to the target.
Ghost Cursor replicates this behavior automatically. The overshootThreshold option controls when overshoot kicks in (default: 500 pixels).
Random target points
Real users don't click the mathematical center of buttons.
Ghost Cursor picks a random point within the element's bounding box, making each interaction unique.
Your first Ghost Cursor script
Let's build a working script that navigates to a page and clicks a button with human-like movements.
Step 1: Set up the project structure
mkdir ghost-cursor-demo
cd ghost-cursor-demo
npm init -y
npm install ghost-cursor puppeteer
Step 2: Create the basic script
Create a file called scraper.js:
// scraper.js
const puppeteer = require('puppeteer');
const { createCursor } = require('ghost-cursor');
async function run() {
// Launch browser in visible mode to see the cursor
const browser = await puppeteer.launch({
headless: false,
args: ['--window-size=1280,800']
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
// Create the ghost cursor attached to this page
const cursor = createCursor(page);
// Navigate to a test site
await page.goto('https://example.com');
// Wait for the page to load
await page.waitForSelector('a');
// Click the link with human-like movement
await cursor.click('a');
console.log('Clicked the link with human-like movement!');
// Keep browser open to see the result
await new Promise(resolve => setTimeout(resolve, 3000));
await browser.close();
}
run().catch(console.error);
Step 3: Run and observe
node scraper.js
Watch the browser window. You'll see the cursor move in a curved path toward the link, then click.
Compare this to standard Puppeteer clicking—the difference is immediately visible.
Step 4: Enable the visible cursor helper
Ghost Cursor can display a visible cursor indicator for debugging:
const cursor = createCursor(page, { visible: true });
This injects a small red dot that follows the cursor position, making it easier to debug movement patterns.
Core features
Ghost Cursor provides several methods for controlling mouse behavior.
Moving the cursor
The move() method moves the cursor to an element without clicking:
// Move to an element by CSS selector
await cursor.move('#submit-button');
// Move with options
await cursor.move('#submit-button', {
paddingPercentage: 20, // Stay within 80% of element center
moveSpeed: 800, // Slower movement
moveDelay: 500, // Pause after moving
maxTries: 5 // Retry attempts
});
The paddingPercentage option controls how close to the center the cursor lands. A value of 0 means anywhere within the element; 100 means exactly center.
For moving to specific coordinates (not elements), use moveTo():
// Move to exact coordinates
await cursor.moveTo({ x: 500, y: 300 });
// Move relative to current position
await cursor.moveBy({ x: 100, y: 50 });
Clicking elements
The click() method combines moving and clicking:
// Basic click
await cursor.click('#login-button');
// Click with options
await cursor.click('#login-button', {
hesitate: 200, // Wait 200ms before clicking
waitForClick: 100, // Hold mouse down for 100ms
moveDelay: 300, // Wait 300ms after moving before clicking
button: 'left', // Mouse button: left, right, middle
clickCount: 1 // Single click (use 2 for double-click)
});
The hesitate option simulates a human pausing before clicking—like when reading button text.
For clicking without moving (when cursor is already positioned):
await cursor.move('#button');
// Do something else...
await cursor.click(); // Click at current position
Scrolling
Ghost Cursor handles scrolling to bring elements into view:
// Scroll element into view
await cursor.scrollIntoView('#footer-section');
// Scroll with options
await cursor.scrollIntoView('#target', {
scrollSpeed: 50, // 0-100, where 100 is instant
scrollDelay: 300, // Wait after scrolling
inViewportMargin: 100 // Extra margin around element
});
// Scroll to specific position
await cursor.scrollTo({ x: 0, y: 1000 });
// Scroll to page top or bottom
await cursor.scrollTo('top');
await cursor.scrollTo('bottom');
// Scroll by relative amount
await cursor.scroll({ y: 500 }); // Scroll down 500px
Generating custom paths
You can generate path data without executing movements:
const { path } = require('ghost-cursor');
const from = { x: 100, y: 100 };
const to = { x: 600, y: 700 };
// Generate basic path
const route = path(from, to);
console.log(route);
// Returns array of {x, y} points
// Generate path with timestamps
const timedRoute = path(from, to, { useTimestamps: true });
console.log(timedRoute);
// Returns array of {x, y, timestamp} points
// Custom path options
const customRoute = path(from, to, {
spreadOverride: 15, // Control curve spread
moveSpeed: 1000 // Affect point density
});
This is useful for:
- Debugging movement patterns
- Pre-calculating paths
- Using Ghost Cursor outside of Puppeteer (gaming, testing, etc.)
Advanced techniques
Once you've mastered basics, these techniques improve your scraping stealth.
Combining with random delays
Ghost Cursor handles mouse movement, but add human-like delays between actions:
function randomDelay(min, max) {
return new Promise(resolve => {
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
setTimeout(resolve, delay);
});
}
async function fillForm(cursor, page) {
// Click first field
await cursor.click('#username');
await randomDelay(100, 300);
// Type with realistic timing
await page.keyboard.type('myusername', { delay: 50 + Math.random() * 100 });
await randomDelay(200, 500);
// Move to next field
await cursor.click('#password');
await randomDelay(100, 200);
await page.keyboard.type('mypassword', { delay: 50 + Math.random() * 100 });
await randomDelay(300, 600);
// Click submit
await cursor.click('#submit');
}
Random movements between actions
Real users move their mouse randomly while reading pages.
Enable random movements to simulate this:
const cursor = createCursor(page, {
performRandomMoves: true // Enable random movements
});
// Or toggle during runtime
cursor.toggleRandomMove(true);
// Do your scraping...
cursor.toggleRandomMove(false);
Handling hover states
Some elements require hovering before clicking (dropdown menus, tooltips):
// Move and wait for hover effect
await cursor.move('#dropdown-trigger');
await page.waitForSelector('.dropdown-menu', { visible: true });
await randomDelay(200, 400);
await cursor.click('.dropdown-option');
Working with iframes
Ghost Cursor works with iframe content through Puppeteer's frame handling:
// Get the iframe
const frameElement = await page.$('iframe');
const frame = await frameElement.contentFrame();
// Create cursor for the frame
const frameCursor = createCursor(frame);
// Now interact with iframe content
await frameCursor.click('#button-inside-iframe');
Proxy integration
Ghost Cursor works with Puppeteer's proxy configuration. When your datacenter IP gets flagged, residential proxies help:
const browser = await puppeteer.launch({
headless: false,
args: [
'--proxy-server=http://proxy.example.com:8080'
]
});
const page = await browser.newPage();
// Authenticate if required
await page.authenticate({
username: 'proxy_user',
password: 'proxy_pass'
});
const cursor = createCursor(page);
// Continue as normal...
Ghost Cursor for Playwright
Ghost Cursor was built for Puppeteer, but several community ports exist for Playwright.
ghost-cursor-playwright
The most maintained Playwright port:
npm install ghost-cursor-playwright
Usage:
const { chromium } = require('playwright');
const { createCursor } = require('ghost-cursor-playwright');
async function run() {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
const cursor = createCursor(page);
await page.goto('https://example.com');
await cursor.actions.click({ target: 'a' });
await browser.close();
}
run();
Note the API differs slightly—Playwright ports use cursor.actions.click() instead of cursor.click().
Python alternatives
For Python developers using Playwright or Pyppeteer:
pip install python-ghost-cursor
Usage:
import asyncio
from playwright.async_api import async_playwright
from python_ghost_cursor.playwright_async import create_cursor
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
page = await browser.new_page()
cursor = create_cursor(page)
await page.goto('https://example.com')
await cursor.click('a')
await browser.close()
asyncio.run(main())
@extra/humanize plugin
For Puppeteer Extra or Playwright Extra users:
npm install puppeteer-extra @extra/humanize
This plugin automatically augments page.click() with human-like movements:
const puppeteer = require('puppeteer-extra');
const { HumanizePlugin } = require('@extra/humanize');
puppeteer.use(HumanizePlugin());
const browser = await puppeteer.launch();
const page = await browser.newPage();
// Regular click is now humanized automatically
await page.click('#button');
Common errors and troubleshooting
"Cannot read property 'evaluate' of undefined"
This happens when the page navigates away before Ghost Cursor finishes:
// Problem: Page might navigate during movement
await cursor.click('#login-button');
// Solution: Wait for navigation explicitly
await Promise.all([
page.waitForNavigation(),
cursor.click('#login-button')
]);
"Element is not visible or not an HTMLElement"
The element exists but isn't clickable:
// Solution 1: Wait for element to be visible
await page.waitForSelector('#button', { visible: true });
await cursor.click('#button');
// Solution 2: Scroll into view first
await cursor.scrollIntoView('#button');
await cursor.click('#button');
Movement looks too fast or too slow
Adjust the moveSpeed option:
// Slower, more deliberate movement
await cursor.click('#target', { moveSpeed: 500 });
// Faster movement
await cursor.click('#target', { moveSpeed: 2000 });
Lower values = slower movement. The default is randomized.
Cursor gets stuck or doesn't reach target
Increase maxTries or check for overlapping elements:
await cursor.click('#target', { maxTries: 15 });
// Or check what's actually at the coordinates
const element = await page.evaluateHandle((x, y) => {
return document.elementFromPoint(x, y);
}, 500, 300);
console.log(await element.evaluate(el => el.tagName));
Debugging movement paths
Enable logging to see what Ghost Cursor is doing:
On macOS/Linux:
DEBUG="ghost-cursor:*" node scraper.js
On Windows PowerShell:
$env:DEBUG = "ghost-cursor:*"
node scraper.js
This outputs detailed movement information to the console.
Best practices
Follow these guidelines for reliable Ghost Cursor usage.
1. Always wait for elements
Never assume elements are ready:
// Good
await page.waitForSelector('#button', { visible: true });
await cursor.click('#button');
// Bad - might fail if page is still loading
await cursor.click('#button');
2. Add realistic delays between interactions
Humans don't perform actions instantly:
await cursor.click('#username');
await page.keyboard.type('user@example.com', { delay: 80 });
await new Promise(r => setTimeout(r, Math.random() * 500 + 200));
await cursor.click('#password');
3. Don't over-optimize movement speed
Faster isn't always better. Suspicious speed patterns trigger detection:
// Natural-feeling speeds
await cursor.click('#target', { moveSpeed: 600 + Math.random() * 400 });
4. Combine with other stealth measures
Ghost Cursor handles mouse behavior only. Complete protection requires:
- Stealth plugins for browser fingerprinting
- Proper user agents and headers
- Residential proxies for IP reputation
- Realistic navigation patterns
5. Test with visible cursor first
Always develop with headless: false and the cursor visible:
const cursor = createCursor(page, { visible: true });
Switch to headless only after confirming movement patterns look correct.
6. Handle errors gracefully
Wrap interactions in try-catch blocks:
async function safeClick(cursor, selector, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
await cursor.click(selector);
return true;
} catch (error) {
console.log(`Click attempt ${i + 1} failed:`, error.message);
await new Promise(r => setTimeout(r, 1000));
}
}
return false;
}
FAQs
Is Ghost Cursor free?
Yes. Ghost Cursor is open source under the MIT license. You can use it in commercial projects without cost.
Does Ghost Cursor work with headless browsers?
Yes. Ghost Cursor generates movement data and triggers mouse events regardless of headless mode. However, some anti-bot systems detect headless browsers through other signals, so you may still need stealth plugins.
Can Ghost Cursor solve CAPTCHAs?
Not directly. Ghost Cursor makes mouse movements human-like, which can help with checkbox-style CAPTCHAs (like "I'm not a robot"). However, it won't solve image-based or puzzle CAPTCHAs.
For CAPTCHA challenges, you'll need dedicated solving services.
Ghost Cursor vs puppeteer-extra-plugin-stealth?
They solve different problems. Puppeteer stealth hides browser automation signals (like navigator.webdriver). Ghost Cursor handles mouse behavior patterns. Use both together for comprehensive protection.
Does Ghost Cursor work with Selenium?
Ghost Cursor is built for the Puppeteer/Playwright ecosystem. For Selenium, look at similar libraries like selenium-stealth or implement Bezier curve movement manually.
Why do some sites still detect me with Ghost Cursor?
Modern anti-bot systems use multiple detection vectors:
- Browser fingerprinting
- TLS fingerprinting
- IP reputation
- Request patterns
- JavaScript execution analysis
Ghost Cursor addresses only mouse behavior. You need a complete stealth stack for protected sites.
How does Ghost Cursor compare to manual Bezier curve implementation?
Ghost Cursor adds several refinements beyond basic Bezier curves:
- Fitts's Law for realistic timing
- Automatic overshoot/correction on long distances
- Random target point selection within elements
- Speed variation based on element size and distance
- Integration with Puppeteer's page object
Building this yourself is possible but time-consuming.
Conclusion
Ghost Cursor transforms robotic browser automation into natural-looking interactions.
By generating human-like mouse movements with Bezier curves and Fitts's Law, it defeats behavioral analysis that catches standard automation tools.
Key takeaways:
- Install with
npm install ghost-cursor puppeteer - Create cursors with
createCursor(page) - Use
cursor.click()andcursor.move()instead of native Puppeteer methods - Combine with delays, stealth plugins, and proxies for comprehensive protection
- Enable
visible: trueduring development to verify movement patterns
Ghost Cursor handles one critical layer of bot detection. Pair it with proper browser stealth and proxy management for production scraping.