← Back to Blog

PinchTab Only Shoots One Viewport: Teaching a 9.4k-Star Browser Bridge to Capture Full Pages

When I want Claude to read a page it can’t fetch (an SPA that renders in JS, a site behind a login wall that bounces WebFetch), I screenshot it with PinchTab and hand over the image. PinchTab is a 9.4k-star open-source browser-automation bridge (Go, MIT): a single ~16MB binary that runs a headless (or headed) Chrome behind a local HTTP API, binds to 127.0.0.1 by default, and holds a persistent profile so the logged-in session survives between calls. /navigate opens a URL, /screenshot returns a JPEG, /evaluate runs JS in the page, /snapshot returns the accessibility tree as text.

It has one gap that matters for reading long pages, and it’s the same gap that sent me benchmarking Berkeley’s pixelshot in the companion post: /screenshot captures the current viewport and nothing else.

One viewport is the whole story

There’s no fullPage option. I checked. Appending ?fullPage=true and similar query guesses doesn’t change the resolution. You get one screen, top-aligned. On a landing page that’s the whole thing. On a long article it’s the top 30%; on the Wikipedia comparison table I was testing, the top 10%. Claude reads the header and the first few rows and never learns the rest of the page exists.

For a while I lived with it; most pages I hand to Claude are short. But the moment the task is “read this whole reference table” or “summarize this long report,” one viewport stops being a minor limitation and becomes the wrong 90% missing.

scroll+shot: measure position, not height

The naive fix is “ask the page how tall it is, then tile from that number.” That’s exactly the approach that makes tools truncate on lazy-load and CSS overflow pages: the up-front height read comes back wrong and everything downstream inherits the lie. The companion post shows pixelshot doing this to an 18,609px Wikipedia page it thought was 1,553px.

So I don’t trust the height. I trust the scroll position. Scroll down a viewport at a time, shoot each frame, stop at the bottom. Lazy content loads because you actually arrive at it; overflow containers scroll because you’re scrolling them.

PinchTab makes this easy because /evaluate runs arbitrary JS in the live page. Read the two numbers that matter, compute the scroll stops with a small overlap so nothing hides in the seam, and flush to the true bottom at the end:

# document height and viewport height, straight from the DOM
docH=$(curl -s -X POST http://localhost:9867/evaluate \
  -H 'Content-Type: application/json' \
  -d '{"expression":"document.documentElement.scrollHeight"}' \
  | jq -r '.result')
winH=$(curl -s -X POST http://localhost:9867/evaluate \
  -H 'Content-Type: application/json' \
  -d '{"expression":"window.innerHeight"}' \
  | jq -r '.result')

# scroll stops, 200px overlap; last stop is pinned to the bottom
positions=$(python3 -c "
d=$docH; w=$winH; ov=200; s=w-ov; y=0; ps=[]
while y+w<d: ps.append(y); y+=s
ps.append(max(0, d-w))
print(' '.join(str(p) for p in ps))")

i=0
for y in $positions; do
  curl -s -X POST http://localhost:9867/evaluate \
    -H 'Content-Type: application/json' \
    -d "{\"expression\":\"window.scrollTo(0, $y)\"}" > /dev/null
  sleep 0.6                       # let lazy content settle
  n=$(printf "%02d" "$i")
  curl -s http://localhost:9867/screenshot | jq -r '.base64' \
    | base64 -d > "shot_${n}.jpg"
  i=$((i+1))
done

Two details carry the whole thing. The 200px overlap (s = w - ov) means consecutive frames share a strip, so a heading that lands on a viewport boundary appears whole in at least one shot instead of getting sliced. The sleep 0.6 is the lazy-load settle: scroll, wait for content to paint, then shoot. Drop it and you capture the skeleton before the section fills in.

On the Wikipedia table this produced 16 frames covering the full page, where the height-detection approach stopped at one.

The frames read sharper than you’d expect

Each frame comes out at 2560×1353px. Claude’s vision model downscales anything past 1568px on the long edge (Sonnet/Haiku), so these do get shrunk, but starting from a 2K frame and scaling down leaves crisper text than starting from a 1280px-wide tile. Counterintuitively, the “unoptimized” high-res capture reads better after downscale than a capture pre-sized to the model’s limit. More source pixels to spend on the shrink.

When you want text, not pixels: /snapshot

Screenshots are the right tool when layout carries meaning: tables, charts, anything visual. When you just need the words, shooting pixels is wasteful: the model burns vision tokens decoding an image back into text it could have read directly.

That’s what /snapshot is for. It returns the page’s accessibility tree, and pulling the StaticText nodes gives you the body copy (the post text of an X thread, the content of a Notion page) without a single screenshot. PinchTab’s own docs put the text path at 5-13x cheaper than the screenshot path for the same content. So the rule I settled on: /snapshot when the answer is words, scroll+shot when the answer is layout.

Folded into a three-mode skill

Hand-assembling curl calls got old, so I collapsed the whole thing into one skill with three modes:

modewhat it does
pinchtab-shot <url>one viewport shot (default)
pinchtab-shot <url> --scrollscroll+shot with 200px overlap, full-page coverage
pinchtab-shot <url> --snapshotextract StaticText (X posts, Notion bodies)

The default stays cheap for short pages; --scroll is the long-page path from this post; --snapshot is the text path. One command picks the capture strategy to match what the page actually is.

Wrap-up

  • PinchTab is a 9.4k-star OSS browser bridge that’s genuinely good at the hard part (rendering SPAs, holding a logged-in profile, running JS in the live page), but /screenshot only captures one viewport, so long pages get read from the neck up.
  • The fix isn’t a taller screenshot, it’s scroll+shot: drive scrollTo through /evaluate, shoot each frame with a 200px overlap, settle 0.6s for lazy-load, flush to the bottom. Trust the scroll position, never a single up-front height read.
  • Higher native resolution (2560px) reads sharper after Claude’s downscale than tiles pre-sized to the 1568px limit.
  • Use /snapshot when you want words, not pixels: 5-13x cheaper than screenshots for the same text.
  • For where the height-detection approach silently truncates (including a Wikipedia page pixelshot read as 1/12th its real size), see the companion post.

Written by @kenimo49 / kenimoto.dev