Skip to content

windowctl

Put every window exactly where it belongs — from a script, from Go, or from plain English. One static binary, three platforms, no daemon.
Terminal window
npm install -g @muthuishere/windowctl
windowctl move --app "Google Chrome" --zone 1A # left half
windowctl move --app "Slack" --monitor 2 --zone 2B # top-right of the second display

That is the whole product surface for the common case. No config file, no daemon to keep running, no menu-bar app. A window manager you can call from a shell script, a Makefile, a Go program, or an AI agent.

Platforms
3macOS · Windows · Linux, one codebase
Install
1 binaryno runtime, no daemon, no postinstall
Screenshot
110 ms1920×1080 via ScreenCaptureKit (spike S5)
Stream round-trip
0.047 msp50, 10 ms audio frame on loopback (spike S0)

A zone is a name for a rectangle. You never compute pixels, and you never care which monitor is which resolution.

1A
1B
–zone 1ALeft half
2A
2B
2C
2D
–zone 2BTop-right quarter
1
2
3
–zone 3:2Middle third — any N:M split

Omit --monitor and the window stays on the display it is already on — resolved by majority overlap, not by guessing from its top-left corner. Pass --monitor 2 and it moves there and snaps. Monitors are 1-indexed by ascending (X, Y), so the leftmost display is always 1, and the numbering survives a reboot or a re-plug.

monitor 1laptop
code
term
monitor 2external · active
jira
chrome
slack
logs

windowctl batch takes a JSON array and applies the whole arrangement — the thing you actually want when you sit down to work.

Terminal window
cat work-mode.json | windowctl batch --json
[
{ "app": "Google Chrome", "monitor": 2, "zone": "2B" },
{ "app": "Code", "monitor": 1, "zone": "1A" },
{ "title": "Terminal", "monitor": 1, "zone": "1B" },
{ "app": "Slack", "monitor": 2, "zone": "2C" }
]

Every entry reports its own result, so a layout that half-applies tells you exactly which window resisted — including when the OS clamps a move rather than honouring it:

windowctl: requested 512x640 at (3840,30), OS clamped to 576x615 at (3840,55)
(likely a minimum-window-size constraint)

It reads the desktop, not just writes to it

Section titled “It reads the desktop, not just writes to it”
Terminal window
$ windowctl windows list --app "Google Chrome" --json
[
{
"ID": 24815, "App": "Google Chrome", "Title": "windowctl — GitHub",
"Bounds": { "X": 1920, "Y": 25, "W": 1920, "H": 1055 },
"Monitor": 2, "Focused": true
}
]

Monitor and Focused are stamped on the unfiltered list before your filter runs — so they mean the same thing no matter what you filtered out.

The agent skill ships inside the binary. No package to find, no repo to clone, no network:

Terminal window
windowctl install skills

“split chrome left, slack right” · “send vscode to my external” · “put everything back to work mode”

It writes window-ctl-skill into ~/.claude/skills/ (and ~/.agents/skills/ when Codex is installed). The skill is the routing layer; every action is still one windowctl call you could have typed yourself.

The same binary is a headless robot body for testing apps that use the mic, camera, screen and keyboard — over a loopback WebSocket with a token and a per-stream ticket.

Terminal window
windowctl type "hello" windowctl screenshot --out shot.png
windowctl key cmd+shift+t windowctl click --x 400 --y 300
windowctl serve --port 9099 windowctl start video-out --port 9100

This half is newer, and the honest state of it is:

SurfaceStateDetail
Keyboard, mouse, chords, dragworkingCGEvent injection, verified round-trip
Screenshot + screen captureworkingScreenCaptureKit; 1920×1080 in 110 ms
Audio device list / set defaultworkingCoreAudio; reversible, restores the prior default
Speaker loopback (audio-out)frames flowProcess tap delivers samples; non-silent audio needs the system-audio permission
Virtual microphone (audio-in)driver builtOur own CoreAudio plugin; loading it needs a notarized Developer ID build
Virtual camera (video-in)not builtCoreMediaIO extension, designed in ADR 0006
Device layer on Windowsnot builtWindow control is fully supported there; the device layer is macOS-only so far

Every row is backed by a runnable spike in the repo and an ADR saying why the decision went that way — including the ones that came back negative.

  • A tiny adapter surface. Six primitives per OS (ListWindows, ListMonitors, Move, Focus, and two permission calls). Everything that can be composed from those lives in the portable layer, so zone maths, monitor resolution, and filter semantics are identical on every platform and unit tested once.
  • CGO where it counts, nowhere else. macOS talks to CoreGraphics and the Accessibility API directly — no AppleScript, no helper app, no private SPI. Windows is user32.dll.
  • Permissions are explicit. On macOS the Accessibility prompt fires from windowctl permissions or lazily from a move — never from windows list. A denied permission is a descriptive error, never a silent no-op.
  • Specs before code. Every capability has an ADR, a runnable spike that proved it on real hardware, and an OpenSpec slice — then an implementation.

MIT licensed. Start here →