This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Gesture Reels — a Manifest V3 Chrome/Edge extension for hands-free control of Instagram Reels and YouTube Shorts via webcam air gestures. All hand tracking runs locally (MediaPipe HandLandmarker, WASM + model bundled in vendor/); nothing is sent to a server. No build step, no git repo.
node test_gestures.mjs # run the pure gesture-logic self-checkLoad/reload in browser: chrome://extensions → Developer mode → Load unpacked (this folder). After editing, hit the reload icon on the extension card; content-script changes also need a page refresh on instagram.com/youtube.com.
There is no bundler, linter, or test framework — test_gestures.mjs is a plain Node script asserting against gestures.js. package.json only sets {"type":"module"} so the .mjs import resolves.
Three isolated execution contexts, connected by postMessage / chrome.runtime messaging:
content.js(injected into instagram.com / youtube.com) — owns page UI and drives the site. Adds the floating "👋 Hands-Free" toggle + a draggable/resizable camera iframe. Receives gesture actions and calls a per-site adapter to act on the page.camera.html+camera.js(extension-origin iframe,web_accessible_resources) — owns the webcam and the MediaPipe inference loop. Classifies each frame viagestures.js, posts{source:"gesture-reels", gesture}to the parent content script. Draws the fingertip ghost trail. Killed (camera + inference stop) the moment the iframe is removed.background.js(service worker) — toolbar icon click → sendsgesture-reels-togglemessage to the active tab's content script.
gestures.js is the one pure, DOM-free, MediaPipe-free module: classify(landmarks) → gesture name, and createEngine(onGesture) → stateful hold/repeat timers. This is what test_gestures.mjs exercises; keep it pure so it stays testable in Node.
- Brittle site selectors live at the top of
content.jsin theyoutubeandinstagramadapter objects. When either site changes its DOM and a gesture stops working, patch there — nowhere else.clickNear()scopes clicks to the active video's ancestor subtree so the wrong reel's button isn't hit;activeVideo()picks the most-visible<video>. - Gesture tuning (hold durations, which finger pose maps to which action) lives in
gestures.jsconstants (POINT_HOLD_MS, etc.) andcreateEngine'sholdsmap. Onlypointrepeats while held; the rest fire once per hold. vendor/is the offline MediaPipe tasks-vision bundle (vision_bundle.mjs,wasm/,hand_landmarker.task). CSP allows'wasm-unsafe-eval'for it; don't add remote script sources.
- Content script matches
instagram.com/*andyoutube.com/*, but each adapter'smatches()further gates the toggle to/reelsand/shortsrespectively (both are SPAs —content.jspolls the URL every 500ms to show/hide the button). rules.json(declarativeNetRequest) strips Instagram'spermissions-policyresponse header so the extension iframe can use the camera;host_permissionsis Instagram-only for this reason.- Camera permission is granted to the extension origin (the iframe), not the host site — so it survives across reels and both sites.