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.
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
| Field | Type | Description |
|---|---|---|
id | String | UUID generated at migration start. Unique per migration. |
sourceSerialNumber | String | Hardware serial number of the source Mac, read from IOKit. |
sourceHostname | String | Hostname of the source Mac (e.g., "James-MacBook-Pro"). |
destinationSerialNumber | String | Hardware serial number of the destination Mac. |
destinationHostname | String | Hostname of the destination Mac. |
timestampStart | String | ISO 8601 UTC timestamp when the user started the transfer. |
timestampEnd | String | ISO 8601 UTC timestamp when the transfer finished (success, failure, or cancel). |
categoriesTransferred | Array | One entry per data category that was selected for migration. |
categoriesTransferred[].categoryID | String | Machine-readable category identifier. |
categoriesTransferred[].categoryName | String | Human-readable category name. |
categoriesTransferred[].sizeBytes | Integer | Total bytes transferred for this category. |
categoriesTransferred[].filesCount | Integer | Number of files transferred in this category. |
categoriesTransferred[].status | String | success — all files transferred; failed — category-level failure; skipped — user or admin excluded it; partial — some files failed. |
connectionType | String | The connection type used: wifi, ethernet, or thunderbolt. |
profileApplied | Boolean | true if an MDM configuration profile was detected and applied. |
profileIdentifier | String? | The PayloadIdentifier of the applied profile, or null if none. |
overallStatus | String | completed — 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
POSTwithContent-Type: application/json - Return a
2xxstatus 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.