Automate / Hooks
Run a script when a recording finishes.
The hook every other recipe builds on. Ten minutes to install; then every finished call runs whatever you put in a folder.
RecordCue has no “run script after recording” checkbox, and does not need one. Its read-only CLI lists only recordings that are safely written, and attaches the transcript to the same record when macOS has finished it. So a job that asks the CLI once a minute, and remembers what it has already seen, is a hook — one whose every line you can read. This page is that job, tested, plus the two other ways to get the same signal.
What the hook receives
The runner puts every executable file in ~/.config/recordcue/actions to work, once per finished recording, in name order. Each action gets the recording's full JSON on standard input and these variables in its environment:
RC_TRANSCRIPT | Path to the .txt transcript, or empty when there is none. |
|---|---|
RC_MEDIA | Path to the recording, .m4a for audio or .mp4 for screen. |
RC_SERVICE | Google Meet, Slack, Zoom… or Recording for a manual one. |
RC_TITLE | The window title, for window recordings. Empty otherwise. |
RC_WHEN, RC_DATE | Local start time as 2026-09-08 18:07, and its date alone. |
RC_MINUTES | Duration, rounded to whole minutes. |
RC_STARTED_AT | The start as ISO 8601 UTC, exactly as the CLI prints it. |
RC_ID | The recording's stable id — the key the runner deduplicates on. |
RC_JSON | The whole record, the same as latest --json, for jq. |
An action that exits 0 is done for that recording. One that fails is retried next minute, alone. Turn on automatic transcription in Settings so RC_TRANSCRIPT is filled in; without it the hook still fires, two minutes after the recording ends, with the media path only.
Install it
Three files: the runner, a launchd agent that runs it every minute, and a folder for actions. Needs jq (brew install jq; recent macOS ships one). Nothing here touches RecordCue's own files.
mkdir -p ~/.config/recordcue/actions
curl -fsSL https://www.recordcue.app/automate/files/hooks.sh \
-o ~/.config/recordcue/hooks.sh
chmod +x ~/.config/recordcue/hooks.sh
cat > ~/Library/LaunchAgents/app.recordcue.hooks.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key> <string>app.recordcue.hooks</string>
<key>ProgramArguments</key> <array>
<string>/bin/zsh</string>
<string>$HOME/.config/recordcue/hooks.sh</string>
</array>
<key>StartInterval</key> <integer>60</integer>
<key>RunAtLoad</key> <true/>
<key>StandardOutPath</key> <string>$HOME/.config/recordcue/hooks.log</string>
<key>StandardErrorPath</key><string>$HOME/.config/recordcue/hooks.log</string>
</dict></plist>
EOF
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/app.recordcue.hooks.plistThe runner itself, as served from /automate/files/hooks.sh. Read it before you run it; it is short on purpose.
#!/bin/zsh
# recordcue hooks — run every action in ~/.config/recordcue/actions once per
# finished recording. launchd starts this every minute; when nothing is new
# it exits at once. Read-only towards RecordCue: it only asks the app's CLI
# what has finished. https://www.recordcue.app/automate/hooks/
set -u
export PATH="/opt/homebrew/bin:/usr/local/bin:$HOME/.local/bin:$PATH"
rc="${RECORDCUE_APP:-/Applications/RecordCue.app}/Contents/MacOS/RecordCue"
actions="$HOME/.config/recordcue/actions"
seen="$HOME/.config/recordcue/seen"
mkdir -p "$actions"
# Exit 3 means "no recordings yet"; treat every non-zero exit as "nothing to do".
json=$("$rc" --agent-cli list --since 1d --json 2>/dev/null) || exit 0
# First run: remember what already exists so old calls are not replayed.
if [[ ! -e "$seen" ]]; then
jq -r '.recordings[] | .id // .media.path' <<<"$json" >"$seen"
exit 0
fi
jq -c '.recordings[]' <<<"$json" | while IFS= read -r rec; do
id=$(jq -r '.id // .media.path' <<<"$rec")
grep -qxF -- "$id" "$seen" && continue
transcript=$(jq -r '.transcript.path // empty' <<<"$rec")
if [[ -z "$transcript" ]]; then
# No text yet. Wait while transcription is still working. If nothing is
# working on it (turned off, or it failed), give the app two minutes to
# queue one before treating the recording as final.
state=$(jq -r '.transcription.state // "none"' <<<"$rec")
[[ "$state" == queued || "$state" == running ]] && continue
if [[ "$state" == none ]]; then
settled=$(jq -r '(now - ((.endedAt // .updatedAt) | fromdateiso8601)) > 120' <<<"$rec")
[[ "$settled" == true ]] || continue
fi
fi
# What every action can rely on. RC_JSON is the full record, as printed by
# `latest --json`; the rest are the fields a shell script reaches for.
export RC_JSON="$rec"
export RC_ID="$id"
export RC_TRANSCRIPT="$transcript"
export RC_MEDIA=$(jq -r '.media.path' <<<"$rec")
export RC_SERVICE=$(jq -r '.service // "Recording"' <<<"$rec")
export RC_TITLE=$(jq -r '.title // empty' <<<"$rec")
export RC_MINUTES=$(jq -r '((.durationMs // 0) / 60000) | round' <<<"$rec")
export RC_STARTED_AT=$(jq -r '.startedAt // empty' <<<"$rec")
export RC_WHEN=$(jq -r '(.startedAt // .updatedAt) | fromdateiso8601 | localtime | strftime("%Y-%m-%d %H:%M")' <<<"$rec")
export RC_DATE="${RC_WHEN%% *}"
# Each action runs once per recording. One that fails is retried on the
# next round without re-running the ones that succeeded.
for action in "$actions"/*(N.x); do
key="$id ${action:t}"
grep -qxF -- "$key" "$seen" && continue
if "$action" <<<"$rec"; then
print -r -- "$key" >>"$seen"
else
print -u2 -- "$(date '+%F %T') ${action:t} failed for ${RC_MEDIA:t}; will retry"
fi
done
doneThe first action, and the test
Start with the one that proves the chain works and asks nothing of the internet:
#!/bin/zsh # ~/.config/recordcue/actions/10-notify.sh # The smallest useful action: a notification that the transcript is ready. osascript -e "display notification \"$RC_SERVICE · $RC_MINUTES min · transcript ready\" \ with title \"RecordCue\""
chmod +x ~/.config/recordcue/actions/10-notify.sh # Run the hook now instead of waiting for the next minute: launchctl kickstart -k gui/$(id -u)/app.recordcue.hooks # Make one recording, stop it, wait for the transcript. Then: tail ~/.config/recordcue/hooks.log cat ~/.config/recordcue/seen
The log stays empty when everything works; only failures are written to it. seen gains one line per finished recording, and one per action that succeeded. Actions run with Homebrew and ~/.local/bin on the path, so claude, codex, gh and whisper-cli are reachable from launchd.
Now add the actions you actually want
Each of these is a file in the same folder, numbered so they run in the order you choose. They are independent: any one can fail and be retried without the others repeating.
Prefer Hazel? Use the companion file.
Turn on Write JSON companion files beside recordings in Settings → AI tools and RecordCue writes <recording>.recordcue.json once the media is safely finished, then rewrites it when the transcript is attached. Hazel can match on that, or more simply on the transcript itself, which arrives last:
#!/bin/zsh
# Hazel rule on ~/Movies/RecordCue (with "Run rules on folder contents" so
# the per-service subfolders count). Condition: Extension is "txt".
# Action: Run shell script — embedded script, passed the file as $1.
rc='/Applications/RecordCue.app/Contents/MacOS/RecordCue'
media="${1%.txt}.m4a"; [[ -e "$media" ]] || media="${1%.txt}.mp4"
json=$("$rc" --agent-cli inspect "$media" --json) || exit 0
# ...the same actions as above, with the JSON in $json and the text in $1.The companion uses paths relative to its own folder, so a recording and its JSON can be moved together without breaking. The CLI reference documents every field.
Want it instant? fswatch.
A minute is usually fine; a transcript takes a minute or two anyway. If you want the hook to fire the moment a file lands, have fswatch call the same runner:
brew install fswatch
fswatch -0 -r --event Created --event Renamed ~/Movies/RecordCue |
while read -d '' path; do
# Any change under the folder — a new recording, a transcript landing,
# a companion file being rewritten — runs the same runner at once.
~/.config/recordcue/hooks.sh
doneRemove it
launchctl bootout gui/$(id -u)/app.recordcue.hooks rm ~/Library/LaunchAgents/app.recordcue.hooks.plist # Keep or delete ~/.config/recordcue as you like.
Have your agent install it
Paste into Claude Code, Codex or any local agent that can fetch a page and run a shell on this Mac.
Read https://www.recordcue.app/automate/hooks/ and install the RecordCue hook runner on this Mac exactly as that page describes: the runner at ~/.config/recordcue/hooks.sh, the launchd agent, and the notification action as the first action. Show me each file before writing it, then run the test steps and show me the log.
Questions
- Does RecordCue have a built-in "run script after recording" setting?
- No. RecordCue exposes two signals instead: its read-only CLI lists only finished recordings, and an optional .recordcue.json companion file can be written beside each finished recording. The runner on this page turns the first into a hook with launchd; Hazel or fswatch can use the second. Either way the script that runs is yours to read.
- Why poll the CLI instead of watching the folder?
- Recordings are filed into one folder per service, and launchd WatchPaths does not watch subfolders. A transcript also arrives a little after the media, so a folder watcher fires before there is text to work on. Asking the CLI once a minute costs almost nothing, sees every folder, and knows whether transcription is still running.
- What if an action fails?
- It is retried on the next run, and only that action. The runner remembers success per recording per action in a plain text file, so a Slack outage does not re-file your Obsidian note. Failures are appended to hooks.log with the action name and the recording.
- Will installing the hook replay old recordings?
- No. On its first run the runner marks everything that already exists as seen and does nothing else. Only recordings finished after that trigger actions.