# Todoist Deploy Script
One-shot installer that sets up the venv, config, and launchd agent for the Todoist sync. Save as `deploy-todoist-sync.sh` (see [[09 Todoist Integration]]).
```bash
#!/usr/bin/env bash
# =====================================================================
# Deploy the Obsidian <-> Todoist sync for a single vault.
# =====================================================================
# The vault syncs its Activities/Tasks/ folder to a Todoist project
# (the project name below must match the one in Todoist).
#
# The sync runs as a launchd agent with its own config dir, state file,
# log, and sync token.
#
# Safe to re-run (idempotent):
# * the venv is only created if missing
# * an existing config.json is LEFT AS-IS (your token is never clobbered)
# * the launchd agent is unloaded + reloaded each run so plist/script
# edits take effect
#
# Usage:
# ./deploy-todoist-sync.sh # prompts for the token
# TODOIST_TOKEN=abc123 ./deploy-todoist-sync.sh # non-interactive
#
# NOTE: the project is also the DEFAULT_PROJECT_NAME baked into
# todoist_sync.py, so the project_name written below is just a
# belt-and-suspenders confirmation.
# =====================================================================
set -euo pipefail
VENV="$HOME/.venvs/todoist-sync"
ICLOUD="$HOME/Library/Mobile Documents/iCloud~md~obsidian/Documents"
LAUNCH_AGENTS="$HOME/Library/LaunchAgents"
# vault-folder | config-name | todoist-project | launchd-label
#
# To sync MULTIPLE vaults (one Todoist project per vault), add one row
# per vault — each gets its own config dir, state file, log, and agent.
# Example for three vaults:
# VAULTS=(
# "MyVault|obsidian-todoist-sync|Obsidian|local.obsidian-todoist-sync"
# "WorkVault|work-todoist-sync|Work|local.work-todoist-sync"
# "HomeVault|home-todoist-sync|Home|local.home-todoist-sync"
# )
VAULTS=(
"YOUR_VAULT|obsidian-todoist-sync|Obsidian|local.obsidian-todoist-sync"
)
# --- 1. Todoist API token ---
TOKEN="${TODOIST_TOKEN:-}"
if [ -z "$TOKEN" ]; then
read -r -s -p "Paste your Todoist API token (Todoist > Settings > Integrations > Developer): " TOKEN
echo
fi
[ -n "$TOKEN" ] || { echo "No token provided; aborting." >&2; exit 1; }
# --- 2. venv (created once; the agent uses this Python binary) ---
if [ ! -x "$VENV/bin/python" ]; then
command -v uv >/dev/null 2>&1 || {
echo "uv is not installed. Install it first: brew install uv" >&2
exit 1
}
echo "Creating venv at $VENV ..."
uv venv --python 3.12 "$VENV"
uv pip install --python "$VENV/bin/python" requests watchdog python-frontmatter
else
echo "venv already present at $VENV"
fi
mkdir -p "$LAUNCH_AGENTS"
# --- 3. Per-vault config + launchd agent ---
for entry in "${VAULTS[@]}"; do
IFS='|' read -r vault cfg proj label <<<"$entry"
scripts="$ICLOUD/$vault/Scripts"
cfgdir="$HOME/.config/$cfg"
plist_src="$scripts/$label.plist"
plist_dst="$LAUNCH_AGENTS/$label.plist"
if [ ! -f "$plist_src" ]; then
echo "[$vault] SKIP — plist not found: $plist_src" >&2
continue
fi
mkdir -p "$cfgdir"
if [ -f "$cfgdir/config.json" ]; then
echo "[$vault] config.json already exists — leaving token as-is"
else
cat >"$cfgdir/config.json" <<EOF
{
"todoist_token": "$TOKEN",
"project_name": "$proj"
}
EOF
chmod 600 "$cfgdir/config.json"
echo "[$vault] wrote $cfgdir/config.json (project: $proj)"
fi
cp "$plist_src" "$plist_dst"
launchctl unload "$plist_dst" 2>/dev/null || true
launchctl load "$plist_dst"
echo "[$vault] loaded launchd agent: $label"
done
cat <<EOF
Done. Verify it is running (it should show a numeric PID):
launchctl list | grep todoist-sync
If the row shows "-" instead of a PID, check the stderr log, e.g.:
cat ~/.config/obsidian-todoist-sync/stderr.log
IMPORTANT — one-time Full Disk Access grant:
launchd-spawned processes can't read iCloud Drive until you grant FDA to
the venv's python binary:
System Settings > Privacy & Security > Full Disk Access > +
then Cmd-Shift-G and paste: $VENV/bin/python
Toggle it on, then re-run this script (or reload the agent).
FIRST-RUN NOTE: on first start, every Task note in the vault that has no
todoist_id: yet becomes a NEW Todoist task in the project. Delete or
archive any Task notes you don't want pushed before running.
EOF
```