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.