Skip to content

Keep secrets out

dotsync’s rule is simple: secrets never leave the machine. They aren’t encrypted into the repository; they’re kept out of it. Before any file is uploaded, dotsync checks its path and every line of its content against a set of rules for private keys, cloud credentials and tokens.

Adding a file that looks like a credential is refused:

dotsync add ~/.aws/credentials
dotsync: /Users/you/.aws/credentials: refusing to manage it: path looks like a secret (matches '.aws/credentials') (use -allow-secrets if the remote is meant to hold it)

A managed file that gains a secret, say after you paste a token into your shell configuration, becomes BLOCKED. Its new version isn’t sent, the version already in the repository stays as it was, and sync and status tell you why:

dotsync sync
  BLOCKED   ~/.config/fish/config.fish  (content looks like a GitHub token)
dotsync: 1 blocked
dotsync status
…
SOURCE            TARGET                      STATUS   DESCRIPTION
fish/config.fish  ~/.config/fish/config.fish  BLOCKED  Fish shell configuration
                                              ↳ content looks like a GitHub token
gitconfig         ~/.gitconfig                ok       Git identity and aliases

BLOCKED files look like they contain secrets, so their changes stay on this machine.
  remove the secret, or mark a false positive with a `dotsync:allow-secret` comment on that line:
  https://pungoyal.github.io/dotsync/guides/secrets/

Remove the secret and the file syncs again on the next run. dotsync doctor also counts blocked files.

Inside a managed directory, credential files such as ~/.config/gh/hosts.yml are held back the same way, while the rest of the directory syncs. add warns about them, and status shows them as BLOCKED until you ignore them.

The clean fix is to keep each secret in a separate, unmanaged file, and have the synced file load it:

~/.config/fish/config.fish (synced)
test -f ~/.config/fish/secrets.fish; and source ~/.config/fish/secrets.fish
~/.config/fish/secrets.fish (this machine only, never added)
set -gx GITHUB_TOKEN ghp_…

If the synced file is inside a managed directory, as secrets.fish would be when all of ~/.config/fish is managed, also ignore it: dotsync set ~/.config/fish --ignore secrets.fish. Its name would keep it from being uploaded anyway, but it would show up as BLOCKED on every sync.

Better still, fetch secrets when they’re needed, from a password manager or the OS keychain: op read, pass, security find-generic-password on macOS, secret-tool on Linux. A value read from a variable or a command isn’t a secret as far as the rules are concerned:

~/.zshrc
export GITHUB_TOKEN=$(op read op://dev/github/token) # fine: read at runtime
export GITHUB_TOKEN=ghp_… # blocked

Files encrypted with age or sops sync like any other file, even when their name looks like a secret, as with .env.json or secrets.yaml. Only ciphertext reaches the repository. The age private key that decrypts them is always refused, so copy it to each new machine yourself. Encrypted files lists exactly what is checked, and Use with mise shows a complete setup.

Sometimes a line only looks like a secret. You have two ways out:

  • One line: put dotsync:allow-secret anywhere on it, usually in a comment. Here the value names a keychain item rather than holding a key, but it looks like an assignment of one:

    api_key = keychain:openai # dotsync:allow-secret
  • A whole entry: add it with dotsync add --allow-secrets, or turn the rules off for an existing entry with dotsync set <entry> --allow-secrets (and back on with --allow-secrets=false).

The rules match patterns: a secret in a format they don’t know gets through. They reduce the risk of a mistake; they don’t make the repository a safe place for credentials. Keep the repository private, and treat everything in it as readable by anyone who can read the repository. See the security model.