{"slug":"self-hosted-app-secrets-icloud-sync","url":"https://tako.sh/blog/self-hosted-app-secrets-icloud-sync/","canonical":"https://tako.sh/blog/self-hosted-app-secrets-icloud-sync/","title":"Encrypted App Secrets in Git, Keys in iCloud","date":"2026-05-07T04:55","description":"Tako lets you commit encrypted app secrets to git while decryption keys stay local, sync through iCloud Keychain, or move by export/import.","author":null,"image":"16daaacdbe32","imageAlt":null,"headings":[{"depth":2,"slug":"the-file-is-portable-the-key-is-not","text":"The file is portable. The key is not."},{"depth":2,"slug":"sharing-is-a-key-operation-not-a-file-copy","text":"Sharing is a key operation, not a file copy."},{"depth":2,"slug":"on-macos-keys-can-follow-your-macs","text":"On macOS, keys can follow your Macs."},{"depth":2,"slug":"rotation-does-not-need-a-redeploy","text":"Rotation does not need a redeploy."},{"depth":2,"slug":"small-enough-to-use-strict-enough-to-trust","text":"Small enough to use, strict enough to trust."}],"markdown":"Secrets are easy until a second laptop shows up.\n\nOne machine has the production database URL. Another needs to deploy. CI needs the same key for release builds. A teammate joins and asks where the Stripe token lives. The usual answer is a `.env` file, a password manager note, a Slack message you promise to delete, or a vault service that is technically correct and operationally one more thing.\n\nWe already wrote about [why Tako does not inject secrets through `.env` files](/blog/secrets-without-env-files/). That post covered the runtime side: AES-256-GCM at rest, fd 3 at spawn time, and typed accessors from `tako generate`.\n\nThis is the other half: sharing the keys that decrypt those secrets. Tako secrets now have stable per-environment key IDs, self-contained key export/import, passphrase-derived keys, and optional iCloud Keychain storage on macOS. The goal is boring on purpose: encrypted project state can live in git, while the key material follows the people and machines that are allowed to use it.\n\n## The file is portable. The key is not.\n\nWhen you run [`tako secrets set`](/docs/cli/), Tako writes encrypted values to `.tako/secrets.json`. That file is meant to be tracked. `tako init` updates `.gitignore` so the app's `.tako/` directory stays ignored while `.tako/secrets.json` remains visible to git.\n\nThe file looks like this:\n\n```json\n{\n  \"production\": {\n    \"key_id\": \"0123456789abcdef\",\n    \"secrets\": {\n      \"DATABASE_URL\": \"base64(nonce + ciphertext + tag)\",\n      \"STRIPE_KEY\": \"base64(nonce + ciphertext + tag)\"\n    }\n  }\n}\n```\n\nSecret names are plaintext so `tako secrets list` can show a useful table without decrypting values. Secret values are encrypted with AES-256-GCM. Each environment has a `key_id`, which is the small but important rebuild: every machine can agree that production uses key `0123456789abcdef`, without storing the key itself in the repo.\n\nThe key lives outside the project:\n\n| Piece                | Where it lives                   | Safe to commit? | What it contains                               |\n| -------------------- | -------------------------------- | --------------- | ---------------------------------------------- |\n| `.tako/secrets.json` | Project repo                     | Yes             | Environment names, key IDs, encrypted values   |\n| Local key file       | Tako data dir, `keys/{key_id}`   | No              | Raw environment key                            |\n| iCloud Keychain item | macOS Keychain                   | No              | Raw environment key, synchronizable            |\n| Exported key bundle  | Clipboard / out-of-band transfer | No              | Base64url JSON with `version`, `id`, and `key` |\n\nThat split is what makes the workflow practical. The encrypted file can move through git like any other project file. The key can move through a different channel, or not move at all if the machine should never decrypt production.\n\n```d2\ndirection: right\n\nrepo: Git repo {\n  shape: rectangle\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\nsecrets: \".tako/secrets.json\" {\n  style.fill: \"#9BC4B6\"\n}\n\nkeyid: \"key_id\" {\n  shape: circle\n  style.fill: \"#E88783\"\n}\n\nstore: \"local file or iCloud Keychain\" {\n  style.fill: \"#FFF9F4\"\n  style.stroke: \"#2F2A44\"\n}\n\nvalue: \"decrypted values\" {\n  shape: hexagon\n  style.fill: \"#9BC4B6\"\n}\n\nrepo -> secrets: \"git pull\"\nsecrets -> keyid: \"points at\"\nkeyid -> store: \"load key\"\nstore -> value: \"decrypt\"\n```\n\n## Sharing is a key operation, not a file copy.\n\nFor a new teammate, the flow is intentionally small:\n\n```bash\n# Person who already has the key\ntako secrets key export --env production\n\n# New machine\ntako secrets key import\n```\n\n`tako secrets key export` reads the cached key for the selected environment, requires macOS user authentication on macOS, and copies one self-contained key bundle to the clipboard. The bundle includes the key ID, so import does not need `--env` when you are importing an exported key. Tako can look at the current project's `.tako/secrets.json`, match the ID, and report the environment name if it finds one.\n\nThere is also a passphrase path:\n\n```bash\ntako secrets key import --passphrase --env production\n```\n\nThat derives the environment key from the passphrase and the environment key ID. It is useful when a tiny team wants a memorized shared secret instead of passing a random key bundle around. If the environment does not have a key ID yet, Tako creates one before saving `.tako/secrets.json`.\n\nBoth paths validate before they trust the key. If the project already has encrypted secrets for that environment, Tako tries to decrypt them with the imported key. Wrong key, wrong passphrase, or corrupted payload: no silent success.\n\nFor CI, the shape is the same. Import the production key into the runner's Tako data directory before the step that needs to decrypt or sync secrets. The repo contains encrypted values; the runner secret store contains the exported key bundle or passphrase. Tako keeps the two concerns separate.\n\n## On macOS, keys can follow your Macs.\n\nLocal key files are still the default because they work everywhere. On macOS, interactive key creation and key import now ask:\n\n```text\nUse iCloud Keychain?\n```\n\nSay yes, and Tako stores the environment key in a synchronizable Keychain item. Pull the repo on another Mac signed into the same iCloud account, and the key can be read from Keychain instead of from `keys/{key_id}`.\n\nThe technical detail is that the macOS CLI is now packaged as a real app bundle, not just a loose executable. The release packaging step copies the Rust binary into `Tako.app/Contents/MacOS/tako`, writes an app `Info.plist`, and signs the bundle with Keychain access-group entitlements. The installer puts `Tako.app` in your Applications directory and symlinks `tako` to the signed executable inside the bundle. From the terminal, it still feels like a normal CLI command; to macOS, it is a signed app allowed to use the protected, synchronizable Keychain.\n\nAt runtime, Tako asks Keychain for a protected generic password item named by the environment key ID and marks it synchronizable. There is no helper daemon, background socket, or side channel. The same process that runs `tako secrets set` or `tako secrets key import` writes the key.\n\nIf the entitlement is unavailable, Tako fails before changing project state:\n\n```text\niCloud Keychain requires the signed Tako app. Reinstall Tako and try again.\n```\n\nThat failure mode is deliberate. If you asked for iCloud storage, Tako should not quietly fall back to a local key file and leave you thinking the key will sync.\n\nThis gives macOS users a nicer day-to-day loop without making Keychain mandatory. Linux servers, CI runners, and teammates on other platforms can keep using local key files and key import.\n\n## Rotation does not need a redeploy.\n\nKey sharing gets the secret onto the right machines. Sync gets it onto the right servers.\n\n```bash\ntako secrets set STRIPE_KEY --env production --sync\n```\n\nor:\n\n```bash\ntako secrets set STRIPE_KEY --env production\ntako secrets sync --env production\n```\n\n[`tako secrets sync`](/docs/cli/) treats the local `.tako/secrets.json` file as the source of truth. For each target environment, Tako decrypts locally using the cached key from iCloud Keychain or `keys/{key_id}`, then sends an `update_secrets` command to `tako-server`.\n\nThe server does not write a remote `.env` file. It stores secrets encrypted in SQLite using a per-device key. Fresh app instances and workflow workers receive secrets through the same fd 3 bootstrap envelope described in [the deployment docs](/docs/deployment/). HTTP instances roll. Workflow workers drain and restart. New processes see the new value; old processes finish what they were already doing.\n\nDeploys use the same model. During [`tako deploy`](/blog/what-happens-when-you-run-tako-deploy/), the CLI asks each server for the app's current secrets hash. If the hash matches, it skips the secrets payload entirely. If the server is new or stale, the deploy includes decrypted secrets and the server stores the update.\n\nThat is the whole shape:\n\n| Task           | Command                                            | Result                                                |\n| -------------- | -------------------------------------------------- | ----------------------------------------------------- |\n| Add a secret   | `tako secrets set API_KEY --env production`        | Encrypts into `.tako/secrets.json`                    |\n| Share access   | `tako secrets key export --env production`         | Copies a key bundle for that environment              |\n| Join a machine | `tako secrets key import`                          | Caches the imported key locally or in iCloud Keychain |\n| Rotate live    | `tako secrets set API_KEY --env production --sync` | Updates servers and rolls fresh processes             |\n| Audit names    | `tako secrets list`                                | Shows presence across environments, never values      |\n\n## Small enough to use, strict enough to trust.\n\nThis is still not a replacement for every vault. Big organizations have approval flows, audit systems, HSMs, break-glass policies, and compliance boxes to check. Those tools are real for a reason.\n\nTako is aiming at the very common middle: teams deploying apps to their own servers who want something better than `.env`, but do not want secret management to become the largest system in the room. A tracked encrypted file, stable environment key IDs, explicit key import/export, passphrases when you want them, iCloud Keychain when your Mac can use it, and fd 3 when the process starts.\n\nSecrets are one of the platform pieces Tako keeps close to the app: routing, TLS, deploys, logs, local dev, workflows, and now a key-sharing path that does not require pretending a plaintext file in `.gitignore` is infrastructure. Start with the [CLI reference](/docs/cli/), skim [how Tako works](/docs/how-tako-works/), or read the original [Secrets Without `.env` Files](/blog/secrets-without-env-files/) if you want the runtime half of the story."}