How to Use Ghost Cursor for Web Scraping in 5 Steps

Ghost Cursor is a handy Node.js library that lets you mimic real, human-like mouse movements while scraping with tools like Puppeteer. Unlike those rigid, bot-like mouse jumps that jump instantly from point A to B, Ghost Cursor plots a smooth, curved trail based on Bezier curves and Fitts’s Law. The result? Your scraper looks far more human — and has a better chance of slipping under the radar of modern anti-bot systems.

Why You Should Get Comfortable with Ghost Cursor

Today’s websites aren’t making scraping easy. They’re deploying advanced anti-bot defenses that can spot traditional automation tools like Puppeteer and Selenium from a mile away — mostly because those tools generate stiff, predictable cursor paths. Straight lines, fixed speeds, zero randomness — the kind of behavior no real person would ever replicate.

Ghost Cursor solves that problem by generating organic mouse paths that feel real. The cursor drifts, curves, and even overshoots like a distracted user would. By taking the time to learn how Ghost Cursor works — and applying it properly — you can make your scrapers tougher to detect and avoid unnecessary blocks.

The difference is measurable. By calculating the points along a curve based on Bezier paths and Fitts’s Law, Ghost Cursor makes sure the mouse movements line up with natural human motion. Those small details help you bypass the kinds of behavior checks that usually flag bots.

Step 1: Install and Set Up Ghost Cursor

First things first — you need to get Ghost Cursor installed and wired into your project. It’s built for Node.js and works perfectly alongside Puppeteer for browser automation.

Installation Process

Create a new folder for your scraper and set up your project:

mkdir ghost-cursor-scraper
cd ghost-cursor-scraper
npm init -y

Pull in Ghost Cursor and Puppeteer as dependencies:

npm install ghost-cursor puppeteer

Basic Project Structure

Set up your main scraper file to initialize your browser and the Ghost Cursor instance:

// scraper.js
const { createCursor } = require('ghost-cursor');
const puppeteer = require('puppeteer');

async function initializeScraper() {
  const browser = await puppeteer.launch({ 
    headless: false,  // Set to true for production
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });
  
  const page = await browser.newPage();
  const cursor = createCursor(page);
  
  return { browser, page, cursor };
}

module.exports = { initializeScraper };

Pro Tips:

  • Always keep headless: false while developing so you can see the cursor in action.
  • Use Chrome DevTools to watch requests and verify that the cursor triggers real events.
  • Start with a proxy early on to dodge IP blocks.

Watch Outs:

  • Don’t install Ghost Cursor globally — keep it scoped to your project.
  • Don’t overload your machine with too many scraper instances.
  • Never forget await — Ghost Cursor needs it for smooth execution.

Step 2: Create Your First Human-Like Scraper

With setup done, you can now test Ghost Cursor by writing a simple scraper that navigates a site and simulates human actions.

Basic Implementation

// basic-scraper.js
const { createCursor } = require('ghost-cursor');
const puppeteer = require('puppeteer');

async function humanLikeScraper(url) {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  const cursor = createCursor(page);
  
  try {
    // Navigate to the target site
    await page.goto(url, { waitUntil: 'networkidle2' });
    
    // Wait for the page to load completely
    await page.waitForTimeout(2000);
    
    // Human-like click on a button
    const buttonSelector = '#submit-button';
    await page.waitForSelector(buttonSelector);
    await cursor.click(buttonSelector);
    
    // Human-like scrolling
    await cursor.move({ x: 400, y: 300 });
    await page.evaluate(() => window.scrollBy(0, 300));
    
    // Extract data after interactions
    const data = await page.evaluate(() => {
      return document.querySelector('.result-data')?.textContent;
    });
    
    console.log('Extracted data:', data);
    
  } catch (error) {
    console.error('Scraping error:', error);
  } finally {
    await browser.close();
  }
}

// Usage
humanLikeScraper('https://example.com');

Understanding Ghost Cursor Methods

The cursor.move() method can deliberately overshoot or miss slightly — just like a real user adjusting the mouse to hit a small target. Here’s a quick breakdown:

Core Methods:

  • cursor.click(selector) — Move and click on an element.
  • cursor.move(selector | coordinates) — Slide the cursor smoothly to a point.
  • cursor.scroll(options) — Perform scrolling with natural pacing.

Tweak Movements:

// Move with custom options
await cursor.move(selector, {
  moveSpeed: 1000,      // Slower speed
  overshootThreshold: 500,  // When to overshoot
  moveDelay: 100,       // Pause after move
  maxTries: 5           // Retry limit
});

Quick Tips:

  • Stick to cursor.click() instead of separate move and click calls — it’s more convincing.
  • Mix up speeds to avoid being too robotic.
  • Always wait for elements to appear before acting.

Step 3: Master Advanced Movement Techniques

Ghost Cursor isn’t just about simple moves and clicks — you can level up your scraper’s realism by using its more advanced tricks.

Custom Path Generation

Use Ghost Cursor’s path function to build custom movement flows:

const { path } = require('ghost-cursor');

// Generate custom movement path
const from = { x: 100, y: 100 };
const to = { x: 600, y: 700 };
const route = path(from, to, {
  spreadOverride: 10,    // How much the path spreads
  moveSpeed: 1000,       // Speed of movement
  useTimestamps: true    // Add timing data
});

// Apply custom path
async function customMovement(page, cursor) {
  for (const point of route) {
    await page.mouse.move(point.x, point.y);
    if (point.timestamp) {
      await page.waitForTimeout(50); // Small pause
    }
  }
}

Realistic Interaction Patterns

async function advancedInteractionPattern(page, cursor) {
  // Mimic reading
  await cursor.move({ x: 200, y: 300 });
  await page.waitForTimeout(1500); // Pause like reading
  
  // Hover over links
  const links = await page.$$('a');
  for (let i = 0; i < Math.min(links.length, 3); i++) {
    await cursor.move(links[i], { moveDelay: 800 });
    await page.waitForTimeout(400);
  }
  
  // Scroll naturally
  for (let i = 0; i < 3; i++) {
    await page.evaluate(() => window.scrollBy(0, 200));
    await page.waitForTimeout(1000 + Math.random() * 1000);
  }
  
  // Final click
  await cursor.click('#target-element');
}

Handling Complex Selectors

async function handleComplexElements(page, cursor) {
  // Dropdowns
  await cursor.click('.dropdown-trigger');
  await page.waitForSelector('.dropdown-menu', { visible: true });
  await cursor.click('.dropdown-menu .option[data-value="target"]');
  
  // Human-like typing
  await cursor.click('#input-field');
  await page.type('#input-field', 'Human-like text input', { 
    delay: 100 + Math.random() * 100 
  });
  
  // Uploads
  const fileInput = await page.$('#file-upload');
  await fileInput.uploadFile('./test-file.txt');
}

Tips:

  • Randomize your pauses with Math.random() for natural gaps.
  • Simulate reading by hovering over content.
  • Click slightly different spots inside elements to vary behavior.
  • Pair Ghost Cursor with realistic typing for maximum believability.

Step 4: Implement Stealth Optimizations

Pair Ghost Cursor with broader stealth tactics for a scraper that’s ready for real-world targets.

Proxy Integration

To dodge IP bans, add proxies — datacenter proxies are ideal, but switch to residential if needed.

async function stealthScraperWithProxy(url, proxyConfig) {
  const browser = await puppeteer.launch({
    headless: false,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      `--proxy-server=${proxyConfig.host}:${proxyConfig.port}`,
      '--disable-web-security',
      '--disable-features=VizDisplayCompositor'
    ]
  });
  
  const page = await browser.newPage();
  
  if (proxyConfig.username && proxyConfig.password) {
    await page.authenticate({
      username: proxyConfig.username,
      password: proxyConfig.password
    });
  }
  
  const cursor = createCursor(page);
  
  return { browser, page, cursor };
}

User Agent and Fingerprint Masking

async function setupStealth(page) {
  await page.setUserAgent(
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  );
  
  await page.setViewport({
    width: 1366,
    height: 768,
    deviceScaleFactor: 1
  });
  
  await page.evaluateOnNewDocument(() => {
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined,
    });
  });
  
  await page.setExtraHTTPHeaders({
    'Accept-Language': 'en-US,en;q=0.9',
    'Accept-Encoding': 'gzip, deflate, br',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Connection': 'keep-alive',
    'Upgrade-Insecure-Requests': '1'
  });
}

Combine with Puppeteer Extra

To further shield your scraper, plug Ghost Cursor into Puppeteer Extra’s stealth plugin.

const puppeteerExtra = require('puppeteer-extra');
const stealthPlugin = require('puppeteer-extra-plugin-stealth');
const { createCursor } = require('ghost-cursor');

puppeteerExtra.use(stealthPlugin());

async function maxStealthScraper(url) {
  const browser = await puppeteerExtra.launch({
    headless: false,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });
  
  const page = await browser.newPage();
  await setupStealth(page);
  
  const cursor = createCursor(page);
  
  await page.goto(url);
  await cursor.click('#target-selector');
  
  return { browser, page, cursor };
}

Request-Based Alternative

For lighter tasks, a simple request + Cheerio flow may be enough:

const axios = require('axios');
const cheerio = require('cheerio');

async function requestBasedScraper(url) {
  try {
    const response = await axios.get(url, {
      headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language': 'en-US,en;q=0.5',
        'Accept-Encoding': 'gzip, deflate',
        'Connection': 'keep-alive',
      }
    });
    
    const $ = cheerio.load(response.data);
    
    const data = $('.target-class').map((i, el) => $(el).text()).get();
    
    return data;
  } catch (error) {
    console.error('Request failed:', error);
    return null;
  }
}

Tips:

  • Test your stealth on detection tools like Cloudflare.
  • Rotate your user agent string often.
  • Use residential proxies for hard-to-scrape targets.
  • Keep an eye on block rates — adapt as needed.

Step 5: Handle Edge Cases and Troubleshooting

Even the best setups run into bumps. Here’s how to handle them.

Common Issues and Fixes

elem.remoteObject is not a function — usually a version mismatch.
Solution:

// Temporary fix: change node_modules/ghost-cursor/lib/spoof.js
// Replace .remoteObject().objectId with ._remoteObject.objectId

npm install ghost-cursor@1.3.0 puppeteer@19.11.1

Docker Bugs:

const browser = await puppeteer.launch({
  headless: true,
  args: [
    '--no-sandbox',
    '--disable-setuid-sandbox',
    '--disable-dev-shm-usage',
    '--disable-gpu',
    '--no-first-run',
    '--no-zygote',
    '--single-process'
  ]
});

Still Blocked?
Try deeper fingerprint spoofing:

async function antiDetectionScraper(url) {
  const browser = await puppeteer.launch({ headless: false });
  const page = await browser.newPage();
  
  await page.evaluateOnNewDocument(() => {
    const getContext = HTMLCanvasElement.prototype.getContext;
    HTMLCanvasElement.prototype.getContext = function(type) {
      if (type === '2d') {
        const context = getContext.call(this, type);
        const getImageData = context.getImageData;
        context.getImageData = function(x, y, w, h) {
          const imageData = getImageData.call(this, x, y, w, h);
          for (let i = 0; i < imageData.data.length; i += 4) {
            imageData.data[i] += Math.floor(Math.random() * 10) - 5;
            imageData.data[i + 1] += Math.floor(Math.random() * 10) - 5;
            imageData.data[i + 2] += Math.floor(Math.random() * 10) - 5;
          }
          return imageData;
        };
        return context;
      }
      return getContext.call(this, type);
    };
  });
  
  const cursor = createCursor(page);
  
  await page.goto(url);
  await page.waitForTimeout(2000 + Math.random() * 3000);
  
  await cursor.move({ x: 100, y: 100 });
  await page.waitForTimeout(1000);
  await cursor.move({ x: 200, y: 300 });
  await page.waitForTimeout(800);
  
  await cursor.click('#target-element');
  
  return { browser, page, cursor };
}

Debugging Tricks

Make Cursor Visible:

async function debugWithVisibleCursor(page, cursor) {
  await cursor.installMouseHelper();
  await cursor.click('#element');
  await page.screenshot({ path: 'debug-screenshot.png' });
}

Watch Network Activity:

async function monitorNetworkRequests(page) {
  page.on('request', request => console.log('Request:', request.url()));
  page.on('response', response => console.log('Response:', response.url(), response.status()));
}

Dynamic Content:

async function handleDynamicContent(page, cursor) {
  await page.waitForFunction(() => {
    return document.querySelector('.dynamic-content') !== null;
  }, { timeout: 10000 });
  
  await page.waitForTimeout(1000);
  await cursor.click('.dynamic-button');
}

Scale Performance:

async function optimizedScraper(urls) {
  const browser = await puppeteer.launch({ headless: true });
  const results = [];
  
  const batchSize = 3;
  for (let i = 0; i < urls.length; i += batchSize) {
    const batch = urls.slice(i, i + batchSize);
    const promises = batch.map(url => scrapeUrl(browser, url));
    const batchResults = await Promise.all(promises);
    results.push(...batchResults);
    await new Promise(resolve => setTimeout(resolve, 2000));
  }
  
  await browser.close();
  return results;
}

async function scrapeUrl(browser, url) {
  const page = await browser.newPage();
  const cursor = createCursor(page);
  
  try {
    await page.goto(url);
    await cursor.click('#target');
    const data = await page.$eval('.result', el => el.textContent);
    return data;
  } catch (error) {
    console.error(`Error scraping ${url}:`, error);
    return null;
  } finally {
    await page.close();
  }
}

Quick Tips:

  • Build in retries and error handling.
  • Use page pools to avoid constant new tabs.
  • Keep memory usage in check.
  • Test on multiple sites to stay ready.

Final Thoughts

Ghost Cursor is a game-changer for anyone serious about stealth scraping. By mimicking how people really use their mouse, it helps you sneak past bot defenses that would catch other automation tools.

Of course, no single tool is a silver bullet. For high-stakes scraping, pair Ghost Cursor with good proxies, fingerprint tricks, and natural browsing behavior.

Stay sharp — sites keep improving their detection, so your scraping strategy has to evolve too. For simple tasks, a lightweight request scraper might be enough. For complex flows, Ghost Cursor + Puppeteer is your new best friend.

Marius Bernard

Marius Bernard

Marius Bernard is a Product Advisor, Technical SEO, & Brand Ambassador at Roundproxies. He was the lead author for the SEO chapter of the 2024 Web and a reviewer for the 2023 SEO chapter.