Open-source JSON macro runner for macOS desktop automation.
MacroMac records mouse and keyboard input over live applications, stores it as editable JSON, and replays it with simple synchronization blocks: delays, shell commands, file signals, and polling conditions.
It is universal in the low-level sense: it does not need app-specific APIs, browser extensions, or per-site integrations. If a workflow can be driven by mouse, keyboard, files, and shell commands, Macro can usually represent it.
It is intentionally low-level. MacroMac does not understand UI semantics, recover from changed layouts, or know whether the correct window is focused.
Experimental macOS tooling for personal automation, prototyping, and script-driven desktop workflows.
Most automation tools choose one world:
- app-specific APIs, accessibility trees, or browser automation, which can be robust but require a matching integration;
- consumer macro recorders, which are convenient but opaque and hard to generate or edit as data;
- scripts and local agents, which are programmable but cannot always press real GUI controls.
MacroMac is the narrow bridge:
real GUI input -> editable JSON -> shell/file synchronization -> headless replay
This is the macro-as-code paradigm: record the physical workflow once, then inspect, edit, generate, version, and run it like any other local program.
MacroMac's original value is this small contract: physical desktop input becomes editable data, and external programs can synchronize with it through simple file and shell handoff points.
That shape is useful for GUI-only workflows: games, legacy apps, chat clients, installers, local tools, and sites where API access is unavailable, unwanted, or slower than using the screen.
The goal is not smarter automation inside the core. The goal is honest low-level automation: record real input, expose it as plain data, and let external tools provide intelligence through files, shell commands, condition, or signal.
- Opens a native macOS launcher for choosing, editing, and running macro JSON files.
- Records live mouse, click, and keyboard input into
stepblocks. - Replays real input through macOS event APIs.
- Stores workflows as plain JSON in
blocks. - Supports fixed delays, shell commands, file-change signals, and shell polling conditions.
- Discards human recording time; playback timing is controlled by
pace. - Runs in headless runner and hotkey supervisor modes.
- Lets each macro define its own launch hotkey.
- Not image recognition.
- Not OCR.
- Not accessibility-tree automation.
- Not browser automation like Selenium or Playwright.
- Not a full RPA platform.
- Not an API replacement when a stable API exists.
- Not resilient to arbitrary app layout changes.
- Not safe to run with untrusted JSON.
- macOS 13 or newer.
- Swift 5.9+ / Xcode Command Line Tools.
Install command line tools if needed:
xcode-select --installFor normal use, download the built MacroMac-macos-universal.zip asset from GitHub Releases, unzip it, and open Macro.command.
The GitHub-generated "Source code" archives are not ready-to-run app builds. Use the attached release zip.
The release build is a universal macOS binary for Apple Silicon and Intel Macs. It is unsigned and not notarized, so macOS may require manual approval on first launch.
Macro is powerful because it can observe and synthesize real desktop input.
macOS may ask for:
- Accessibility - allows Macro to move the cursor, click, and type.
- Input Monitoring - allows Macro to observe keyboard and hardware key events.
Grant permissions in
System Settings -> Privacy & Security -> Accessibility / Input Monitoring. Depending on how you launch Macro, macOS may ask you to grant permission to Terminal, the Swift-builtmacrobinary, or both. Relaunch Macro after changing permissions.
Macro JSON is trusted code:
stepblocks can click and type into real applications.commandandconditionblocks run through$SHELL -lc, falling back to/bin/zsh -lc.- Macros can alter the clipboard, move files, delete files, submit forms, or trigger app actions.
Only run macro files you understand. Do not run untrusted JSON.
Recorded macros can also reveal private workflow details: screen coordinates, app layout, keyboard actions, file paths, and shell commands. This repository intentionally ignores macros/*.json.
From the repository root:
swift run macroThis opens the native launcher. From there:
New / Overwriteopens the editor for a new or selected macro.Edit Stepsrewrites only the existingstepblocks in a selected macro.Runstarts the selected macro in runner mode.Open JSONopens the selected macro file in your default editor.Hotkeysstarts the background supervisor for macros with top-levelhotkey.
The launcher reads ./macros/*.json relative to the current working directory.
The repository includes simple double-click launchers:
Macro.command- opens the launcher.Example.command- runs the safe example macro frommacros/example.json.Hotkeys.command- starts the background hotkey supervisor.
These are plain shell scripts. macOS may require confirmation the first time you open them.
The development launchers use swift run, so the development machine needs the Swift toolchain.
To create a folder that can be copied to another Mac without installing Swift there:
./PackageRelease.commandThis creates dist/MacroMac/ and dist/MacroMac-macos-universal.zip with:
macro- release executable.Macro.command- opens the launcher using the bundled executable.Example.command- runs the bundled example macro.Hotkeys.command- starts the bundled hotkey supervisor.macros/example.json- safe sample macro.macro.config.example.json- optional controls template.
The target Mac still needs compatible macOS permissions for Accessibility and Input Monitoring. For public distribution outside your own machines, code signing and notarization are recommended to avoid Gatekeeper warnings.
In the overlay editor:
F1- start or stop recording the currentstep.F2- cancel the current recordedstepand record the same step again.F3- run or stop playback.F4- quit.
In runner mode:
F3- run or stop the loaded macro.F4- quit.
Top-row function keys are fragile on macOS. Depending on keyboard settings and hardware, brightness/Mission Control/Launchpad keys may map to the same physical keys. Input Monitoring permission may be required for reliable handling.
Editor and runner controls can be changed in macro.config.json:
{
"version": 1,
"controls": {
"record": { "key": "F1" },
"cancelStep": { "key": "F2" },
"playback": { "key": "F3" },
"quit": { "key": "F4" }
}
}If macro.config.json is missing, these defaults are used. macro.config.example.json is committed as a template; macro.config.json is local and ignored by git.
Hotkeys use:
{ "key": "F8", "modifiers": ["control", "option"] }or a raw macOS virtual key code:
{ "keyCode": 100, "modifiers": ["control", "option"] }Supported named keys include F1 through F20, A through Z, 0 through 9, space, tab, return, escape, delete, left, right, up, and down. Supported modifiers are command, control, option, shift, and function.
Open launcher:
swift run macroOpen editor directly:
swift run macro -- --editorRecord into a specific file:
swift run macro -- --editor --macro my-flow.jsonRewrite only existing step blocks:
swift run macro -- --editor --rewrite-steps --macro my-flow.jsonRun a macro in headless mode and wait for F3:
swift run macro -- --runner --macro my-flow.jsonRun immediately:
swift run macro -- --runner --play --macro my-flow.jsonStart the hotkey supervisor:
swift run macro -- --hotkeysIn this mode MacroMac scans ./macros/*.json, loads every macro with a top-level hotkey, and keeps running in the background. Pressing a macro hotkey starts that macro. Pressing the same hotkey again stops it. Pressing another macro hotkey stops the current macro and starts the new one.
Restart the supervisor after adding, removing, or changing macro hotkey fields.
Local shortcut mode:
swift run macro -- --dialog dialog.json--dialog is a shortcut for headless runner plus immediate play. If no file is provided, it uses macros/dialog.json. This is intended for local custom scenarios; dialog.json is ignored by git.
- Record one or more
stepblocks in the overlay editor. - Edit the JSON manually or with another tool.
- Add
delay,command,condition, orsignalblocks between steps. - Run the JSON through the launcher, runner, or a
.commandscript. - If the UI moved, use
Edit Stepsto rewrite only the recorded input while keeping the surrounding logic.
The editor records input order, not human timing. If you pause while thinking during recording, that pause is not stored.
A macro is an ordered top-level list of blocks. Most blocks are single actions in that list; repeat contains its own ordered blocks list for a fixed local loop.
{
"version": 3,
"hotkey": {
"key": "F8",
"modifiers": ["control", "option"]
},
"sourceScreen": { "width": 1512, "height": 982 },
"loop": true,
"blocks": [
{
"kind": "step",
"name": "open field",
"pace": 0.1,
"actions": [
{ "kind": "move", "x": 0.5, "y": 0.5 },
{ "kind": "leftClick", "x": 0.5, "y": 0.5 },
{ "kind": "keyDown", "x": 0.5, "y": 0.5, "keyCode": 49, "modifiers": 0 },
{ "kind": "keyUp", "x": 0.5, "y": 0.5, "keyCode": 49, "modifiers": 0 }
]
},
{ "kind": "condition", "command": "test -f ready.txt", "poll": 1, "timeout": 60 },
{ "kind": "command", "command": "cp ready.txt output.txt", "timeout": 10 },
{ "kind": "delay", "seconds": 1 },
{
"kind": "repeat",
"count": 3,
"blocks": [
{ "kind": "command", "command": "date >> ticks.log", "timeout": 10 },
{ "kind": "delay", "seconds": 0.5 }
]
}
]
}If loop is true, playback returns to block 0 after the final block. A pause before the next cycle should be modeled as the final delay.
hotkey is optional. It is used only by --hotkeys; normal editor and runner modes ignore it.
Recorded input: cursor movement, mouse down/up, clicks, and key down/up actions.
Important fields:
actions- ordered input actions.pace- synthetic seconds between actions. Default:0.1.compactMoves- if true, consecutive movement-only runs are compacted.
Playback begins with the first action. Include an initial move action when the cursor should move before the first click or key event.
Slow or animated UI often needs pace around 0.2 to 0.35.
Fixed wait:
{ "kind": "delay", "seconds": 5 }Use this only when fixed time is the right synchronization primitive.
Run one shell command once:
{ "kind": "command", "command": "cp input.txt output.txt", "timeout": 10 }The macro continues only if the command exits with code 0. A non-zero exit stops playback.
Poll a shell command until it exits with code 0:
{ "kind": "condition", "command": "test -s answer.txt", "poll": 1, "timeout": 120 }Use condition when another system needs time to produce a file, text, state, or any shell-checkable result.
Wait for a file to be created or modified:
{ "kind": "signal", "path": "signal.txt", "poll": 0.1 }Macro snapshots file existence, size, and modification time on entry. It continues only after that state changes. Another process can release the macro with:
date +%s%N > signal.txtRepeat one block or a sequence of blocks a fixed number of times:
{
"kind": "repeat",
"count": 5,
"blocks": [
{
"kind": "step",
"name": "click item",
"pace": 0.15,
"actions": [
{ "kind": "leftClick", "x": 0.5, "y": 0.5 }
]
},
{ "kind": "delay", "seconds": 0.25 }
]
}count is the number of local passes. blocks can contain any normal block, including another repeat. If count is 0, the block is skipped. Missing count is treated as 1.
There are two built-in repeat forms:
- Repeat the whole macro forever with top-level
"loop": true. - Repeat a local block sequence N times with a
repeatblock.
Example: run a recorded step, wait, and do that pair four times without looping the whole macro:
{
"kind": "repeat",
"count": 4,
"blocks": [
{
"kind": "step",
"name": "submit once",
"pace": 0.2,
"actions": [
{ "kind": "leftClick", "x": 0.5, "y": 0.5 }
]
},
{ "kind": "condition", "command": "test -f ready-for-next.txt", "poll": 1, "timeout": 60 }
]
}Use a final delay to pause between whole-macro cycles. Use signal or condition when a human, agent, app, or file-producing process decides when to continue. Use command and files for dynamic branching or counters that are more complex than fixed count.
Supported action.kind values inside a step:
moveleftClickrightClickleftDownleftUprightDownrightUpkeyDownkeyUp
Coordinates are normalized from 0 to 1 against the source screen. Playback maps them to the current main display. This helps with screen size differences, but it does not make workflows layout-independent.
For exact playback, keep the same:
- main display
- app/window placement
- focused app
- keyboard layout/input source
- macOS Space/full-screen state
- target app state
Normal recording happens over the real desktop. The overlay ignores mouse events and uses a listen-only event tap, so input continues to reach the real applications. F2 clears only the currently recording step; already saved steps and non-step blocks stay unchanged.
- Main-display oriented.
- Coordinate-based, not semantic UI automation.
- No built-in image recognition or OCR.
- No automatic recovery if a window, dialog, or loading state changes.
- No per-action recorded timing.
- Hotkey events are listen-only; choose shortcuts that do not conflict with the active app.
- Runner errors currently go to system logs / Terminal output, not a rich run report.
- Shell commands are trusted code.
- macOS permissions can silently break capture or playback if not granted.
Macro works best for controlled, repeatable workflows where you can express readiness with files, shell checks, or explicit waits.
This repository ignores generated and personal files:
.build/.DS_Store*.logDialog.commandmacro.config.jsontmp/macros/*.jsonexceptmacros/example.json
Do not commit personal macro JSON or local launcher scripts. Recorded macros may contain private screen coordinates, app workflows, file paths, prompt paths, clipboard logic, or destructive shell commands.
Build:
swift buildTest:
swift testRun launcher:
swift run macroRun the safe committed example:
swift run macro -- --runner --play --macro example.jsonRun a macro immediately:
swift run macro -- --runner --play --macro my-flow.jsonRun the hotkey supervisor:
swift run macro -- --hotkeysRegenerate the README demo GIF:
./scripts/render-demo-gif.commandMacroMac communicates directly with the macOS WindowServer via low-level CGEventCreateKeyboardEvent and CGEventTap APIs:
graph LR
subgraph Macro Schema Parsing
A[JSON Macro Definition] -->|Schema Validation & Guard Checks| B[Macro Execution Engine]
B -->|Nested Frame Stack| C[Repeat & Loop Counter / Max 16 Depth]
end
subgraph CoreGraphics Event Dispatcher
C -->|Raw Keycode + Modifier Mask| D[CGEventCreateKeyboardEvent]
D -->|Atomic Down + 1.2ms Sleep + Up| E[CGEventPost to kCGHIDEventTap]
E -->|Direct Mach Port Injection| F[Target Application Process / Window]
end
To prevent modifier keys (Command, Option, Control, Shift) from getting stuck down during high-speed macro bursts:
// Production C++/Objective-C++ CoreGraphics Event Dispatcher
void DispatchAtomicKeystroke(CGKeyCode keyCode, CGEventFlags flags, useconds_t holdDurationUs = 1200) {
// 1. Create KeyDown event
CGEventRef keyDown = CGEventCreateKeyboardEvent(nullptr, keyCode, true);
CGEventSetFlags(keyDown, flags);
// 2. Create KeyUp event
CGEventRef keyUp = CGEventCreateKeyboardEvent(nullptr, keyCode, false);
CGEventSetFlags(keyUp, flags);
// 3. Post to system HID event tap
CGEventPost(kCGHIDEventTap, keyDown);
if (holdDurationUs > 0) {
usleep(holdDurationUs); // 1.2ms hardware debounce simulation
}
CGEventPost(kCGHIDEventTap, keyUp);
// 4. Guaranteed memory cleanup
CFRelease(keyDown);
CFRelease(keyUp);
}MacroMac requires zero-friction diagnostic preflight checks before starting the background daemon:
| Required Entitlement | Privacy Database Key | Verification API | Failure Behavior |
|---|---|---|---|
| Accessibility | kTCCServiceAccessibility |
AXIsProcessTrustedWithOptions() |
Disables keystroke injection, prompts System Settings |
| Input Monitoring | kTCCServiceListenEvent |
CGEventTapCreate() returns null |
Falls back to active window focus emulation |
| Screen Recording | kTCCServiceScreenCapture |
CGPreflightScreenCaptureAccess() |
Optional: required only for pixel-color trigger conditions |
A real-world example executing an automated build-and-test sequence in Terminal with automated focus restoration:
{
"name": "Quick Release Build & Test",
"version": "1.0",
"target_bundle_id": "com.apple.Terminal",
"steps": [
{
"type": "command",
"action": "focus_target_window"
},
{
"type": "keystroke",
"text": "cargo test --release\n",
"delay_after_ms": 500
},
{
"type": "repeat",
"count": 5,
"interval_ms": 250,
"nested_steps": [
{ "type": "keystroke", "key": "down_arrow" }
]
},
{
"type": "signal",
"event": "build_completed_audio_chime"
}
]
}MIT. See LICENSE.
Macro intentionally stays small. More intelligent layers, such as OCR, image matching, LLM processing, or app-specific logic, should live outside the core and communicate through files, shell commands, condition, or signal.
Разработано и поддерживается Жирняком и Адольфом Петушковым.

