Ghost Cursor Tutorial: Human mouse movements for puppeteer

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 mousemove events
  • 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() and cursor.move() instead of native Puppeteer methods
  • Combine with delays, stealth plugins, and proxies for comprehensive protection
  • Enable visible: true during 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.