Skip to content

Troubleshooting ​

Start with point-bang check (or npm run check). It prints the version, confirms the phone-page files are reachable, and says whether cursor injection is available — which covers most of what follows.

The phone can't reach the PC over WiFi (Windows) ​

The first run raises a Windows Defender Firewall prompt. If it was dismissed, the program is blocked and the page never loads — and in the QR flow the phone's local-network fetch fails the same way, so the page shows the "Couldn't reach the PC" message. Re-allow it for private networks, or use the USB flow, which needs no firewall rule.

Firewall rules are per-executable. npm start working proves nothing about point-bang.exe: the npm flow runs under node.exe, which has its own rule. Worse, denying the prompt once creates a permanent Block rule for that binary — after that, no prompt ever reappears and every connection attempt fails silently. Check in an admin PowerShell:

powershell
# is the exe blocked?
Get-NetFirewallApplicationFilter |
  Where-Object Program -match 'point-bang' |
  Get-NetFirewallRule | Select-Object DisplayName, Action, Enabled

The simplest fix is to wipe every rule bound to the exe so Windows asks again — start point-bang afterwards and this time allow it on private networks (adjust the path to where your exe lives):

powershell
Get-NetFirewallApplicationFilter -Program "C:\path\to\point-bang.exe" |
  Get-NetFirewallRule | Remove-NetFirewallRule

Or skip the prompt entirely and create the Allow rule yourself:

powershell
New-NetFirewallRule -DisplayName "point-bang" -Direction Inbound `
  -Program "C:\path\to\point-bang.exe" -Action Allow -Profile Private,Public

Moving or renaming the exe means a new prompt (and a new rule) — the rule binds to the full path.

QR flow: "Couldn't reach the PC at …" ​

The hosted page could not complete its one signaling fetch to your PC. In rough order of likelihood:

  1. Different networks — phone on mobile data or a guest SSID, PC on ethernet in another subnet. Both must share the LAN.
  2. Firewall — see the Windows entry above.
  3. Permission denied — you dismissed Chrome's local-network prompt. Fix: Chrome → ⋮ → Settings → Site settings → find the page's site → allow Local network access, then reload.
  4. Chrome older than 142 — the Local Network Access exemption doesn't exist yet, so the fetch dies as mixed content. Update Chrome.
  5. Wrong IP first in the QR — VPN or virtual adapters can outrank the real WLAN. The QR carries up to three addresses and the page tries each, but point-bang ip shows what's being offered; --page-url plus a fresh scan after disabling the VPN adapter helps.

The page retries by itself (3s → 6s → 12s backoff) — once the cause is fixed, it connects without a reload.

Signaling worked but the DataChannel never opened — usually client isolation (hotel/office WiFi that blocks phone↔PC traffic). No WiFi setup can cross that policy; USB is the way through.

Linux: "libXtst.so.6: cannot open shared object file" ​

On an X11 session cursor injection uses the XTEST extension. Install it — sudo apt install libxtst6 libx11-6 on Debian/Ubuntu, libXtst on Fedora/Arch. GNOME Wayland does not use it (see the next entry).

Linux (Ubuntu, Fedora): the phone connects but the cursor doesn't move ​

You are on a Wayland session — the default since Ubuntu 22.04. X11 input injection cannot reach a Wayland desktop: it moves a private pointer inside Xwayland, reports success, and the real cursor stays where it is. Versions up to 0.11.2 did exactly that, silently.

Check which session you have with echo $XDG_SESSION_TYPE, then look at the first input: line serve prints:

input: GNOME Wayland — the cursor is driven through GNOME's remote-control API

means the cursor is driven through GNOME Shell itself — no root, nothing to install. GNOME shows its screen-sharing indicator in the top bar for as long as the server runs; nothing is recorded, the stream only serves as the coordinate frame for absolute aim. Stopping the session from that indicator stops the cursor until the server re-creates it a moment later — quit the server (or use the pause hotkey) to really stop.

input: WARNING — Wayland session: the X11 input addon cannot move the real cursor here

means your desktop is Wayland but not GNOME (KDE, sway, …), or GNOME's service did not answer — the line above it says why. Log out, pick an X11 session at the login screen (the gear icon; "Ubuntu on Xorg", "Plasma (X11)") and start the server again.

point-bang check reports the same thing without starting a server, and point-bang monitors lists the monitors as GNOME lays them out — with display scaling these are the scaled sizes, which is the space aim is mapped in.

Linux: "Could not open main display" / the server exits immediately ​

There is no X display to inject into — you're on a headless machine, a container or an SSH session with no DISPLAY. The input driver reacts to this by terminating the process rather than reporting an error, so there is nothing to catch.

Since v0.1 serve detects this and starts in virtual input mode instead of crashing:

input: VIRTUAL — no DISPLAY (headless); aim is printed, the cursor is not moved
input: assuming a 1920x1080 screen (--screen WxH to change)
aim  u=0.512 v=0.334  ->  983,360 px   x[.....+......] y[...+........]

Everything except the actual cursor works: the phone page is served, WebXR calibrates, aim streams in, buttons and jitter stats are reported. Only the final injection step is replaced by a printed line (throttled to 10/s). Use --screen WxH to make the printed pixels match your real monitor, and --input none to force this mode on a machine that does have a display — handy for watching what the phone sends without your cursor running away.

--input native demands the real device regardless; on a display-less box that still ends in the crash above, which is why auto is the default. A machine with no display cannot drive a real cursor at any price.

macOS: the downloaded binary won't start ​

Two separate blocks stand between a fresh download and a running binary, and neither needs sudo. Each has its own symptom:

  • "permission denied" — or even sudo: point-bang-…: command not found — browsers don't preserve the executable bit on downloads. (sudo reports "command not found" because it can't exec the file, which is misleading.)
  • "zsh: killed" or "cannot be opened because the developer cannot be verified" — Gatekeeper's quarantine attribute on downloaded files; macOS may kill an ad-hoc signed binary outright.

Both fixes, in order:

sh
chmod +x ./point-bang-*-macos-*
xattr -d com.apple.quarantine ./point-bang-*-macos-*
./point-bang-*-macos-*

If xattr says "No such xattr", the file simply wasn't quarantined — move on. If macOS still refuses after that, open System Settings → Privacy & Security, scroll down, and click the Open Anyway button naming the binary. Still stuck? Confirm you grabbed the right architecture: file ./point-bang-*-macos-* must say arm64 on Apple Silicon and x86_64 on an Intel Mac.

macOS: check says input is ready but the cursor doesn't move ​

That's TCC: macOS silently drops injected events until you grant Accessibility to the app that runs point-bang — your terminal (or the binary itself) under System Settings → Privacy & Security → Accessibility. Because releases are ad-hoc signed, every update counts as a new app and macOS asks again — expected, not a bug.

macOS: the pause hotkey does nothing ​

Grant Input Monitoring to your terminal (System Settings → Privacy & Security → Input Monitoring) — recent macOS versions gate global key-state reads behind it, and a denial is invisible to the server: the hotkey arms normally and just never reacts, which is why the startup log carries this hint on macOS. The general hotkey notes below apply too.

Port already in use ​

The server never fights another program for its port:

  • Default port (8443) busy → it logs http: port 8443 is busy — using a free port instead and starts on an OS-assigned free port. Nothing else to do: the banner, QR and every printed URL carry the port it actually bound, and the phone connects through those — in adb mode the adb reverse mapping follows the real port too.
  • Explicit --port/PORT busy → it refuses with a one-line error instead of silently moving off the port you pinned. Close the other program (often a second point-bang), pick another port, or use --port 0 to let the OS choose every run.

One caveat: the standalone tunnel command cannot see which port a fallen-back server chose — pass it the port from the server's banner (tunnel --port <n>), or prefer serve --tunnel ngrok, which always tunnels the bound port.

"session key required" / the server logs "connection refused (missing/wrong session key)" ​

The phone tried to connect without this run's session key. The key is minted fresh on every server start and travels inside the QR — so this usually means the phone is holding yesterday's URL (a bookmark, an old tab, a reopened Chrome session). Scan the QR again (or retype the printed URL, #key=… included) and it connects. Restarting the server invalidates the old key on purpose; --key <your-own> keeps a stable key across restarts if you want bookmarks to survive, and --key off disables the check entirely on a network you trust.

You opened the page via plain HTTP over the network. Use the QR flow — the hosted page is HTTPS — or the adb tunnel (localhost is a secure context).

immersive-ar not supported ​

Install or update Google Play Services for AR on the phone.

Hit-test never finds the screen corners ​

Expected on some setups: monitors are emissive, low-texture surfaces that ARCore struggles with — which is why two-ray mode (no surfaces needed) is the default. If you switched to hit-test, switch back. Room clutter around the monitor actually improves tracking; blank walls and whip-pans cause limited/lost states.

Cursor is mirrored or rotated ​

Corners were captured in the wrong order. Recalibrate: top-left, top-right, bottom-left — exactly.

Aspect check is red ​

Sloppy corner captures. Recalibrate and aim more deliberately; in two-ray mode step a full 50cm sideways between the two captures.

Cursor is jittery ​

Move the slider toward its smooth end, check the server's p95 jitter print, prefer USB or 5 GHz WiFi. See Aim & Latency Tuning.

Cursor lags behind your aim ​

Move the slider toward its snappy end (or fully right for raw aim). If WiFi still lags, compare against the USB flow — a big difference means network delay.

Buttons don't appear ​

All buttons — placed ones and the strip — only appear after calibration completes. If they still don't, check the server's startup log for buttons: lines reporting config problems (a bad rect also lands there).

The pause hotkey does nothing ​

Check the server's startup log. pause hotkey: shift+s toggles tracking means it is armed; pause hotkey: unavailable — <reason> tells you why it is not (headless session, no X display, an unwatchable key in the combo). Run point-bang check to see the hotkey status for your install. On Wayland the key state is only visible while an X11/Xwayland window has focus — Proton and X11 games are fine, native Wayland apps are not. Keys with layout-dependent codes (punctuation) cannot be watched; letters, digits, F-keys, modifiers and navigation keys all work.

adb tunnel died ​

adb reverse mappings die on cable replug or adb restart — just re-run npm run start:adb / point-bang serve --mode adb (either re-establishes the tunnel on every start). If it reports a failure, check adb is on your PATH and USB debugging is authorized on the phone.

Released under the MIT License.