Skip to main content

Health sync daemon (disabled)

Disabled

Disabled since February 2026. The LaunchAgent has been unloaded. The daemon was found to import corrupted multi-source sleep data from the CSV pipeline, contributing to inflated sleep debt calculations. The HealthSync iOS app is now the sole health data source with single-source filtering. Code is preserved in GitLab for reference.

info

This page covers the health-sync daemon that imported sleep, activity, and workout data from Apple Health. It is no longer running.

Flow Control series

  1. Flow Control
  2. Architecture
  3. Cursor sync daemon
  4. Music sync daemon
  5. Health sync daemon (disabled) - You are here
  6. HealthSync iOS app (recommended)
  7. Deployment

Why a separate daemon?

Apple Health does not provide an easy way to export data programmatically. I use HealthFit as a bridge to Google Sheets, then export to CSV for import.

ChallengeSolution
No Apple Health API on MacHealthFit iOS app syncs to Google Sheets
Data scattered across formatsGoogle Apps Script exports to CSV daily
macOS CloudStorage restrictionsApple Shortcut copies to accessible folder
Need incremental importsDaemon tracks last import, processes new data

Data flow

Prerequisites

  1. HealthFit app (iOS) - Syncs Apple Health to Google Sheets
  2. Google Drive - Stores exported CSVs
  3. Google Drive for Desktop (Mac) - Syncs CSVs locally
  4. Apple Shortcut - Copies from CloudStorage to accessible folder

Setup

1. Configure HealthFit

  1. Install HealthFit from the App Store
  2. Connect to Google Sheets in Settings
  3. Enable automatic sync on workout completion

This creates a Google Sheets spreadsheet with tabs for:

  • Daily metrics (steps, calories, heart rate)
  • Sleep data (duration, stages, efficiency)
  • Weight measurements
  • Workouts (type, duration, heart rate)

2. Create Google Apps Script

Add a daily export script to your HealthFit spreadsheet:

function exportHealthData() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const folder = DriveApp.getFolderById('YOUR_FOLDER_ID');

const sheets = ['Metrics', 'Sleep', 'Weight', 'Workouts'];
sheets.forEach(name => {
const sheet = ss.getSheetByName(name);
if (sheet) {
// Delete existing file if present
const existing = folder.getFilesByName(name.toLowerCase() + '.csv');
while (existing.hasNext()) {
existing.next().setTrashed(true);
}

// Create new CSV
const csv = convertToCSV(sheet);
folder.createFile(name.toLowerCase() + '.csv', csv, MimeType.CSV);
}
});
}

function convertToCSV(sheet) {
const data = sheet.getDataRange().getValues();
return data.map(row =>
row.map(cell => {
if (cell instanceof Date) {
return Utilities.formatDate(cell, 'UTC', 'yyyy-MM-dd HH:mm:ss');
}
if (typeof cell === 'string' && cell.includes(',')) {
return '"' + cell.replace(/"/g, '""') + '"';
}
return cell;
}).join(',')
).join('\n');
}

Set a time-driven trigger for 5am daily.

3. Configure Google Drive sync

  1. Install Google Drive for Desktop
  2. Configure it to sync your Health export folder
  3. Note the local path (typically ~/Library/CloudStorage/GoogleDrive-*/My Drive/...)

4. Create Apple Shortcut

macOS restricts daemon access to ~/Library/CloudStorage/. Create a shortcut that:

  1. Runs a shell script to copy CSVs
  2. Triggers on a schedule (e.g., 6am daily)

Shell script (~/bin/health-sync.sh):

#!/bin/bash
SOURCE="$HOME/Library/CloudStorage/GoogleDrive-your.email@example.com/My Drive/HealthExports"
DEST="$HOME/health-data"

mkdir -p "$DEST/Health"
mkdir -p "$DEST/Workouts"

# Copy health CSVs
cp "$SOURCE/Health/metrics.csv" "$DEST/Health/" 2>/dev/null
cp "$SOURCE/Health/sleep.csv" "$DEST/Health/" 2>/dev/null
cp "$SOURCE/Health/weight.csv" "$DEST/Health/" 2>/dev/null

# Copy workout CSVs
cp "$SOURCE/Workouts/workouts.csv" "$DEST/Workouts/" 2>/dev/null

echo "Health data copied at $(date)"

5. Build the daemon

cd ~/Projects/tools/health-sync
npm install
npm run build

6. Configure launchd

Create ~/Library/LaunchAgents/com.example.health-sync.plist:

<?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>com.example.health-sync</string>

<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/node</string>
<string>/Users/YOUR_USER/Projects/tools/health-sync/dist/index.js</string>
</array>

<key>EnvironmentVariables</key>
<dict>
<key>STATS_DASHBOARD_URL</key>
<string>https://your-stats.example.com</string>
<key>STATS_API_KEY</key>
<string>your-api-key-here</string>
<key>HEALTH_DATA_DIR</key>
<string>/Users/YOUR_USER/health-data</string>
<key>POLL_INTERVAL</key>
<string>900000</string>
</dict>

<key>RunAtLoad</key>
<true/>

<key>KeepAlive</key>
<true/>

<key>StandardOutPath</key>
<string>/tmp/health-sync.log</string>

<key>StandardErrorPath</key>
<string>/tmp/health-sync.error.log</string>
</dict>
</plist>

7. Load the service

launchctl load ~/Library/LaunchAgents/com.example.health-sync.plist

Configuration

VariableDefaultDescription
STATS_DASHBOARD_URLRequiredDashboard base URL
STATS_API_KEYRequiredAPI key for authentication
HEALTH_DATA_DIR~/health-dataPath to health CSV directory
POLL_INTERVAL900000Sync interval in ms (15 minutes)

CSV sources

CSV FileLocal PathDescription
sleep.csv~/health-data/Health/Sleep records from Oura/Apple Watch
metrics.csv~/health-data/Health/Daily steps, calories, heart rate
workouts.csv~/health-data/Workouts/Exercise sessions

Data imported

Sleep data

FieldDescription
dateSleep date
sleepStartBedtime (HH:MM)
sleepEndWake time (HH:MM)
timeAsleepMinsActual sleep duration
remMinsREM stage duration
deepMinsDeep stage duration
coreMinsLight/Core stage duration
efficiencySleep efficiency percentage
wakeCountNumber of wake-ups

Daily metrics

FieldDescription
dateMetrics date
stepsStep count
activeEnergyActive calories
restingHrResting heart rate
hrvHeart rate variability

Workouts

FieldDescription
dateWorkout date
startTimeStart time (HH:MM)
typeWorkout type
durationMinsDuration in minutes
caloriesCalories burned
avgHrAverage heart rate

Sleep debt calculation

The dashboard calculates sleep debt using a 14-day rolling window:

Sleep Debt = Sum of (Target - Actual) over 14 days

Default target is 7.5 hours (450 minutes). Positive values indicate debt, negative values indicate surplus.

AI health insights

The dashboard generates AI-powered recommendations based on:

  • Last night's sleep (duration, efficiency, stages)
  • Sleep debt trend (improving/worsening/stable)
  • Current work session (hours, late night detection)

Insights are cached and regenerated only when new health data is imported.

Cache invalidation

When the daemon imports new sleep data, the AI insight cache is automatically cleared:

Troubleshooting

No data importing

# Check if CSVs exist
ls -la ~/health-data/Health/
ls -la ~/health-data/Workouts/

# Check daemon logs
tail -100 /tmp/health-sync.log

CloudStorage access denied

If the daemon cannot read from ~/Library/CloudStorage/, use the Apple Shortcut approach to copy files to ~/health-data/ first.

Stale data

The dashboard shows data staleness on the Health page. If data is more than 2 days old:

  1. Check HealthFit sync on iOS
  2. Verify Google Apps Script trigger ran
  3. Confirm Google Drive synced locally
  4. Run the Apple Shortcut manually

Force re-import

To re-import all historical data:

rm ~/.health-sync-state.json
launchctl unload ~/Library/LaunchAgents/com.example.health-sync.plist
launchctl load ~/Library/LaunchAgents/com.example.health-sync.plist

Next: Deployment covers Kubernetes deployment and GitOps configuration.