A macOS LaunchAgent that watches your Obsidian vault's 05-Tasks/ directory
and auto-commits + pushes every change to a git remote. Used as the trigger
for workflow W1 (Obsidian → Morgen).
- launchd runs
src/auto-commit.jsunder Node whenever a file changes inside your watch path, throttled to at most once every 30s and at least once every 5 minutes. - The script runs
git add -A && git commit -m "auto: task edit …" && git push. - The push to GitHub triggers an n8n
githubTriggerwebhook (workflow W1), which parses your task files and mirrors them into Morgen. (Pre-2026-05-04 the kit also wrote to Notion; that leg was dropped.)
The first version of this daemon used a bash script. On macOS that means
granting Full Disk Access to /bin/bash — which transitively grants
FDA to every bash script you ever run, forever. That's unacceptable.
Plan B: install a tiny .app bundle that wraps your Node binary. macOS TCC
identifies binaries by bundle identity, so FDA on this bundle applies only
to this one daemon. The launchd plist points at the bundle's binary, and
launchd invokes the script through it.
This is what install-daemon.sh sets up for you.
- macOS (launchd is a macOS feature).
- Node 18+ in your PATH (or set
NODE_BIN=/absolute/path/to/node). - A local git clone of your vault (the
05-Tasksdir must be a git repo, or be inside one). - A passwordless git remote. The daemon runs headless — it cannot answer
SSH passphrase prompts. Use one of:
gh auth login(recommended) with HTTPS.- An SSH key that's loaded into your agent (
ssh-add). - A PAT stored in the macOS keychain via
git credential-osxkeychain.
BUNDLE_ID=io.example.task-maxxing-daemon \
WATCH_PATH="$HOME/path/to/vault/05-Tasks" \
SCRIPT_PATH="$HOME/code/task-maxxing/src/auto-commit.js" \
bash daemon/install-daemon.shRequired env vars:
| Variable | Purpose |
|---|---|
BUNDLE_ID |
Reverse-DNS label for the launchd agent + Info.plist identifier. |
WATCH_PATH |
Absolute path to the directory launchd should watch (your 05-Tasks). |
SCRIPT_PATH |
Absolute path to src/auto-commit.js from this repo clone. |
Optional env vars:
| Variable | Default | Purpose |
|---|---|---|
NODE_BIN |
$(command -v node) |
Which Node binary to wrap. |
APP_SUPPORT_DIR |
$HOME/Library/Application Support/task-maxxing |
Where the wrapper .app is installed. |
LOG_DIR |
$HOME/Library/Logs |
Where launchd stdout/stderr go. |
The installer will:
- Copy your Node binary into
~/Library/Application Support/task-maxxing/TaskMaxxingDaemon.app/Contents/MacOS/TaskMaxxingDaemon. - Write an
Info.plistfor the bundle. - Render
io.example.task-maxxing-daemon.plist.template→~/Library/LaunchAgents/${BUNDLE_ID}.plist. - Lint the plist with
plutil. - Load it via
launchctl bootstrap gui/$(id -u) …. - Print the Full Disk Access walkthrough.
After the installer finishes, macOS will block the daemon from reading
anything under ~/Desktop, ~/Documents, iCloud Drive, etc. Grant FDA:
-
Open System Settings -> Privacy & Security -> Full Disk Access.
-
Click +.
-
Press Cmd+Shift+G to bring up "Go to folder".
-
Paste the path to the bundle the installer printed (something like
~/Library/Application Support/task-maxxing/TaskMaxxingDaemon.app). -
Select it and click Open.
-
Toggle the entry ON.
-
Reload the agent so launchctl picks up the new permission:
launchctl bootout "gui/$(id -u)/${BUNDLE_ID}" launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/${BUNDLE_ID}.plist"
# Watch the log.
tail -f "$HOME/Library/Logs/task-maxxing.log"
# Touch a tracked file in the watch path.
touch "$WATCH_PATH/README.md"Within 30s (the ThrottleInterval) you should see:
[2026-04-14 11:42:01 EDT] auto-commit: README.md
[2026-04-14 11:42:03 EDT] pushed successfully
If you see a FATAL: cannot read …/.git/HEAD line, FDA isn't actually
granted to the bundle — re-check steps 4–6 above. The log line includes
the exact path to the Node binary you need to grant FDA to (it's inside
the bundle's Contents/MacOS/).
BUNDLE_ID=io.example.task-maxxing-daemon
launchctl bootout "gui/$(id -u)/${BUNDLE_ID}" || true
rm -f "$HOME/Library/LaunchAgents/${BUNDLE_ID}.plist"
rm -rf "$HOME/Library/Application Support/task-maxxing"Then remove the bundle entry from System Settings -> Privacy & Security -> Full Disk Access.
- Nothing commits, log is empty — launchd probably refused to load the
plist. Check
launchctl print gui/$(id -u)/${BUNDLE_ID}andlog show --predicate 'subsystem == "com.apple.xpc.launchd"' --last 10m. - Commits happen but push fails — the log will say
push failed — will retry next tick: …. Usually a credential issue; try runninggit -C "$WATCH_PATH" push origin mainfrom your terminal and see what credential prompt appears. - Heartbeat log is quiet — the daemon isn't firing. Check
StartIntervalandWatchPathsin the installed plist withplutil -p "$HOME/Library/LaunchAgents/${BUNDLE_ID}.plist".