Logging

Every migration produces a structured JSON log. Logs are always saved locally on the Mac. If shft.logMigrationsToEndpoint is configured, the log is also POSTed to that URL after the migration completes.

JSON Schema for the migration log payload
For endpoint developers: drop into a validator or use it to generate types for your ingestion service. Covers every field shft POSTs after a migration.
migration-log.schema.json
Download

Log JSON schema

{
  "id": "string — UUID identifying this migration",
  "sourceSerialNumber": "string — hardware serial of the source Mac",
  "sourceHostname": "string — hostname of the source Mac",
  "destinationSerialNumber": "string — hardware serial of the destination Mac",
  "destinationHostname": "string — hostname of the destination Mac",
  "timestampStart": "string — ISO 8601 timestamp when migration started",
  "timestampEnd": "string — ISO 8601 timestamp when migration completed",
  "categoriesTransferred": [
    {
      "categoryID": "string — e.g. userFiles, applicationData",
      "categoryName": "string — display name e.g. User Files",
      "sizeBytes": "integer — bytes transferred for this category",
      "filesCount": "integer — number of files in this category",
      "status": "string — success | failed | skipped | partial"
    }
  ],
  "connectionType": "string — wifi | ethernet | thunderbolt",
  "profileApplied": "boolean — whether an MDM profile was active",
  "profileIdentifier": "string | null — profile identifier if present",
  "overallStatus": "string — completed | failed | cancelled"
}

Field reference

FieldTypeDescription
idStringUUID generated at migration start. Unique per migration.
sourceSerialNumberStringHardware serial number of the source Mac, read from IOKit.
sourceHostnameStringHostname of the source Mac (e.g., "James-MacBook-Pro").
destinationSerialNumberStringHardware serial number of the destination Mac.
destinationHostnameStringHostname of the destination Mac.
timestampStartStringISO 8601 UTC timestamp when the user started the transfer.
timestampEndStringISO 8601 UTC timestamp when the transfer finished (success, failure, or cancel).
categoriesTransferredArrayOne entry per data category that was selected for migration.
categoriesTransferred[].categoryIDStringMachine-readable category identifier.
categoriesTransferred[].categoryNameStringHuman-readable category name.
categoriesTransferred[].sizeBytesIntegerTotal bytes transferred for this category.
categoriesTransferred[].filesCountIntegerNumber of files transferred in this category.
categoriesTransferred[].statusStringsuccess — all files transferred; failed — category-level failure; skipped — user or admin excluded it; partial — some files failed.
connectionTypeStringThe connection type used: wifi, ethernet, or thunderbolt.
profileAppliedBooleantrue if an MDM configuration profile was detected and applied.
profileIdentifierString?The PayloadIdentifier of the applied profile, or null if none.
overallStatusStringcompleted — migration finished; failed — migration stopped due to error; cancelled — user cancelled.

Example log payload

{
  "id": "8A3B7C2D-4E5F-6789-0ABC-DEF123456789",
  "sourceSerialNumber": "C02XG2DQJGH5",
  "sourceHostname": "James-MacBook-Pro",
  "destinationSerialNumber": "FVFH30KENQ05",
  "destinationHostname": "James-MacBook-Air",
  "timestampStart": "2026-03-18T14:32:00Z",
  "timestampEnd": "2026-03-18T14:58:47Z",
  "categoriesTransferred": [
    {
      "categoryID": "userFiles",
      "categoryName": "User Files",
      "sizeBytes": 34567890123,
      "filesCount": 12847,
      "status": "success"
    },
    {
      "categoryID": "applicationData",
      "categoryName": "Application Data & Preferences",
      "sizeBytes": 2345678901,
      "filesCount": 4521,
      "status": "partial"
    },
    {
      "categoryID": "browserData",
      "categoryName": "Browser Data",
      "sizeBytes": 891234567,
      "filesCount": 3201,
      "status": "success"
    }
  ],
  "connectionType": "thunderbolt",
  "profileApplied": true,
  "profileIdentifier": "com.shft.config.profile",
  "overallStatus": "completed"
}

Setting up a log endpoint

What shft sends

When shft.logMigrationsToEndpoint is configured, shft sends an HTTP POST request after each migration:

POST <configured URL>
Content-Type: application/json

<JSON log body>

Expected response

shft considers the log delivery successful if the endpoint returns any 2xx status code. Non-2xx responses are logged locally but do not affect the migration — the migration is already complete by the time the log is sent.

Endpoint requirements

  • Accept POST with Content-Type: application/json
  • Return a 2xx status code on success
  • Handle payloads up to 100 KB
  • Be reachable from the Mac at the time of migration completion (if the Mac has internet access)

Suggested ingestion options

Simple webhook receiver

A minimal Express.js server that writes logs to a file:

const express = require('express');
const fs = require('fs');
const app = express();
 
app.use(express.json());
 
app.post('/api/shft/migrations', (req, res) => {
  const log = req.body;
  const filename = `migration_${log.id}.json`;
  fs.writeFileSync(`./logs/${filename}`, JSON.stringify(log, null, 2));
  console.log(`Migration logged: ${log.id} (${log.overallStatus})`);
  res.status(200).json({ received: true });
});
 
app.listen(3000, () => console.log('shft log receiver on port 3000'));

Webhook platforms

These platforms can receive shft log webhooks and route them to your existing tools:

  • Tines — create a webhook action, parse the JSON, and route to Slack, Jira, or a database
  • Zapier — use a Webhooks trigger to receive logs and send to Google Sheets, Slack, etc.
  • n8n — self-hosted webhook automation, similar to Zapier
  • AWS API Gateway + Lambda — receive logs and write to DynamoDB or S3

SIEM integration

POST logs to your SIEM's HTTP event collector:

  • Splunk: use the HTTP Event Collector (HEC) endpoint
  • Elastic: use the Elasticsearch bulk API or Logstash HTTP input
  • Datadog: POST to the Datadog Logs API

Local log storage

Regardless of the endpoint configuration, all migration logs are stored locally at:

~/Library/Application Support/shft/logs/migration_<id>.json

These logs persist until manually deleted. They contain the same JSON structure as the POSTed payload.

Privacy considerations

What is included in logs

  • Machine serial numbers and hostnames
  • Category names and sizes
  • File counts per category
  • Transfer timestamps
  • Connection type
  • Success/failure status

What is never included in logs

  • File names — no individual filenames appear in logs
  • File contents — no file data is ever logged
  • File paths — no directory paths appear in logs
  • Keychain items — no passwords, certificates, or secrets
  • User credentials — no login names or passwords
  • IP addresses — not included in the log payload

The logs are designed to answer "what categories moved, how much data, and did it work" without revealing any specific data content.