Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


title: “cottage (ctg)” sub_title: “A modern git-based age-encrypted secrets manager for teams” author: “Arijit Basu cottage@arijitbasu.in” theme: name: dark options: end_slide_shorthand: true h1_slide_titles: true

cottage

cat ../logo/cottage-dark.png

cottage is a modern git-based age-encrypted secrets manager for teams.

This guide covers all subcommands and their options with real examples.

This document is best viewed in a terminal with presenterm.

Note

The term “tracked secret” or just “secret” used in this guide refers to secrets that have been encrypted, i.e., that have a corresponding .cott.age file.


Path Target Behavior

Most ctg commands can take a file or a directory as an argument.

  • File: Operates on that specific file.
  • Directory: Recursively operates on all tracked secrets within that directory.
  • .cott.age file: Usually treated as the source for decryption or the target for status/diff.
  • .cott.toml file: Metadata file. Most commands skip these directly as they are managed alongside the .cott.age files.

ctg init

Initialize cottage in the current directory. This creates a .cottage directory for recipients and identities.

git checkout . -q && ctg clean -qq

ctg init
# (Initializes the .cottage directory)

ctg encrypt

Encrypt files or directories. By default, it processes all the tracked secrets in the entire project root.

git checkout . -q && ctg clean -qq

# First, un-track a secret by deleting the .cott.age file
rm ./secrets/secret.yaml.cott.*

echo "added: line" >> ./secrets/secret.yaml

# Now encrypt the file and start tracking it again
ctg encrypt ./secrets/secret.yaml
# Output:
# encrypt ./secrets/secret.yaml
#    into ./secrets/secret.yaml.cott.age
#    edit ./secrets/secret.yaml.cott.toml

Target Behavior

  • Decrypted File: Encrypts it into a .cott.age file and updates metadata.
  • Directory: Recursively finds and encrypts all decrypted secrets.
  • .cott.age: Skipped (already encrypted).
  • .cott.toml: Skipped (metadata).

ctg decrypt

Decrypt files or directories.

git checkout . -q && ctg clean -qq

# Decrypt a specific secret
ctg decrypt ./secrets/secret.yaml.cott.age
# Output:
# decrypt ./secrets/secret.yaml.cott.age
#    into ./secrets/secret.yaml

Target Behavior

  • .cott.age File: Decrypts it into the corresponding plain-text file.
  • Directory: Recursively finds and decrypts all .cott.age files.
  • Decrypted File: Skipped (already decrypted).
  • .cott.toml: Skipped (metadata).

ctg status

See pending actions based on timestamps.

git checkout . -q && ctg clean -qq

# Check status
ctg status ./secrets/secret.yaml.cott.age
# Output:
# decrypt ./secrets/secret.yaml.cott.age
#    into ./secrets/secret.yaml

# Decrypt and modify to see pending encryption
ctg decrypt ./secrets/secret.yaml.cott.age -qq
echo "status: change" >> ./secrets/secret.yaml

ctg status ./secrets/secret.yaml
# Output:
# encrypt ./secrets/secret.yaml
#    into ./secrets/secret.yaml.cott.age

Target Behavior

  • Any Target: Works on any path that is part of a secret (plain-text or .cott.age) or a directory containing them.

ctg diff

See the actual diff between encrypted and decrypted files. This decrypts the encrypted version in memory (safe from accidental exposure) to compare.

git checkout . -q && ctg clean -qq

# Decrypt and modify a file
ctg decrypt ./secrets/secret.yaml.cott.age -qq
echo "diff: change" >> ./secrets/secret.yaml

# View the diff
ctg diff ./secrets/secret.yaml

Output:

diff --git a/./secrets/secret.yaml b/./secrets/secret.yaml
--- a/./secrets/secret.yaml
+++ b/./secrets/secret.yaml
@@ -1 +1,2 @@
 SECRET: foobar
+diff: change

Target Behavior

  • Any Target: Similar to status, it can be pointed at any file in a secret pair or a directory. It will decrypt the .cott.age file in memory and compare it with the on-disk plain-text file.

ctg verify

Verify the checksum matches for encrypted files and recipients. This is useful in CI to ensure that all changes to secrets and recipients are documented properly in the metadata file.

git checkout . -q && ctg clean -qq

# Verify a specific secret
ctg verify ./secrets/secret.yaml.cott.age
# verified: all encrypted secrets are in sync with metadata

# Try to verify with wrong recipients to see it fail
ctg verify ./secrets/secret.yaml.cott.age -R Cargo.toml
# Output:
# Error: ./secrets/secret.yaml.cott.toml: recipients mismatch: use --skip-verify-recipients to skip this check

Target Behavior

  • .cott.age File: Verifies the content checksum and recipient checksum against the metadata.
  • Directory: Recursively finds and verifies all .cott.age files.
  • Decrypted File: Verifies the corresponding .cott.age file.

ctg sync

Keeps encrypted and decrypted files in sync based on timestamps.

git checkout . -q && ctg clean -qq

# Decrypt and modify
ctg decrypt ./secrets/secret.yaml.cott.age -qq
echo "sync: change" >> ./secrets/secret.yaml

# Sync will encrypt the newer decrypted file
ctg sync ./secrets/secret.yaml
# Output:
# encrypt ./secrets/secret.yaml
#    into ./secrets/secret.yaml.cott.age
#    edit ./secrets/secret.yaml.cott.toml

Target Behavior

  • File/Dir/Age: Syncs the target(s). If a decrypted file is newer, it encrypts. If a .cott.age file is newer, it decrypts.

ctg edit

Edit and encrypt a file directly. Opens it in your default $EDITOR, and re-encrypts it upon saving and exiting.

Run it with --clean to automatically delete the decrypted file after editing.

git checkout . -q && ctg clean -qq

# ctg edit ./secrets/secret.yaml.cott.age --clean # (Opens $EDITOR, then encrypts on save)

echo foo | ctg edit ./secrets/secret.yaml.cott.age --clean

# Output:
# encrypt ./secrets/secret.yaml
#    into ./secrets/secret.yaml.cott.age
#    edit ./secrets/secret.yaml.cott.toml
# delete ./secrets/secret.yaml

Target Behavior

  • Plain Text: Directly opens the decrypted file for editing, then encrypts it after saving.
  • .cott.age: Decrypts for editing, then re-encrypts after saving.
  • Directory: Not supported (must target a specific secret).
  • .cott.toml: Not supported (cannot edit metadata directly).

ctg clean

Delete all decrypted secrets to keep the workspace clean.

Run it with --gitignore to also remove the gitignore entry.

git checkout . -q && ctg de ./secrets/secret.yaml.cott.age -qq

# First, dry run to see what would be deleted
ctg clean . --dry-run
# Output:
# delete ./secrets/secret.yaml

# Actually delete decrypted secrets
ctg clean .
# Output:
# delete ./secrets/secret.yaml

Target Behavior

  • Decrypted File: Deletes it.
  • Directory: Recursively deletes all decrypted secrets within.
  • .cott.age / .cott.toml: Skipped.

Warning

ctg clean is destructive for your local decrypted copies. Always ensure your changes are encrypted before cleaning.


ctg run / ctgx

Decrypt secrets, run a specified command, and automatically delete the decrypted secrets after the command finishes.

git checkout . -q && ctg clean -qq

# Run 'ls' while secrets are temporarily decrypted
ctg run -- ls ./secrets/secret.yaml
ctg run -- ls ./secrets/secret.yaml.cott.age
# Output:
# decrypt ./secrets/secret.yaml.cott.age
#    into ./secrets/secret.yaml
# ./secrets/secret.yaml
# decrypt ./secrets/secret.yaml.cott.age
#    into ./secrets/secret.yaml
# ./secrets/secret.yaml

ctg env

Decrypt an environment file in memory and export its content as environment variables for the specified command.

It defaults to .env.cott.age in the current directory.

git checkout . -q && ctg clean -qq

# Run 'printenv' with variables from .env.cott.age
ctg env -F ./.env.cott.age -- printenv SECRET
# Output:
# foobar

# If the file is not a valid dotenv file, it exports the entire content as COTTAGE_SECRET
ctg env -F ./secrets/secret.yaml.cott.age -- sh -c 'echo "$COTTAGE_SECRET"'
# Output:
# SECRET: foobar

Target Behavior

  • -F, --file: Specifies the encrypted file to use. It must be an encrypted file (ending in .cott.age).

Remote Upstreams

ctg can be configured to pull/push secrets from/to remote upstreams like HashiCorp Vault, AWS Secrets Manager, or custom APIs.

Upstreams are defined in cottage.toml and linked to secrets in their .cott.toml metadata files.

Configuration Example (cottage.toml)

[upstream.customvault]
vars = { HOST = "customvault.example.com" }

[upstream.customvault.pull]
script = 'curl -s "https://${HOST}/api/pull${DESTINATION}"'

[upstream.customvault.push]
script = 'curl -s -X POST -d @- "https://${HOST}/api/push${DESTINATION}"'

Linking a Secret (./secrets/secret.json.cott.toml)

[upstream.customvault]
pull = true
push = true
vars = {
  DESTINATION = "/vault/myapp/env/staging"
}

ctg pull

Fetch secrets from a remote upstream and encrypt them locally.

git checkout . -q && ctg clean -qq

# Pull a secret from the 'customvault' upstream
ctg pull customvault ./secrets/secret.json.cott.age
# Output:
# pull    customvault
#    into ./secrets/secret.json.cott.age
#    edit secrets/secret.json.cott.toml

Target Behavior

  • .cott.age File: Pulls the secret for this specific file if it has an upstream configured in its metadata.
  • Directory: Recursively pulls secrets for all .cott.age files that have upstreams configured.
  • Decrypted File: Pulls for the corresponding .cott.age file.

ctg push

Push locally encrypted secrets (decrypted in memory) to a remote upstream.

git checkout . -q && ctg clean -qq

# Push a secret to the 'customvault' upstream
ctg push customvault ./secrets/secret.json.cott.age
# Output:
# push    ./secrets/secret.json.cott.age
#    into customvault

Target Behavior

  • .cott.age File: Pushes the secret for this specific file if it has an upstream configured in its metadata.
  • Directory: Recursively pushes secrets for all .cott.age files that have upstreams configured.
  • Decrypted File: Pushes for the corresponding .cott.age file.

Common Options

Many ctg commands share these common options:

  • -f, --force: Skip checksum verification and force the operation (e.g., re-encrypt/re-decrypt even if timestamps match).
  • -n, --dry-run: Show what would be done without actually making any changes.
  • -R, --recipients-file PATH: Encrypt to or verify against recipients listed at PATH.
  • --skip-verify-encrypted: Skip checksum verification of encrypted files.
  • --skip-verify-recipients: Skip checksum verification of recipients.
  • --skip-preview: Skip generation of previews for encrypted files.
  • --skip-timestamps: Skip updating timestamps on files after encryption/decryption.
  • --skip-gitignore: Skip adding files to .gitignore.
  • --skip-encryption: Skip operations involving encryption (sync, diff, status).
  • --skip-decryption: Skip operations involving decryption (sync, diff, status).

Command Option Examples

git checkout . -q && ctg clean -qq

ctg decrypt ./secrets/secret.yaml.cott.age --force
# Output:
# decrypt ./secrets/secret.yaml.cott.age
#    into ./secrets/secret.yaml
# (Even if the decrypted file is up-to-date, it will be re-decrypted)

# Dry run: see what would be decrypted
ctg decrypt ./secrets/secret.json.cott.age --dry-run
# Output:
# decrypt ./secrets/secret.json.cott.age
#    into ./secrets/secret.json


# Skip encryption when checking status
echo "change" >> ./secrets/secret.yaml
ctg status ./secrets/secret.yaml --skip-encryption
# (No output, as only pending encryption exists)

ctg autocomplete

Generate shell completions for Bash, Zsh, Fish, etc.

# Generate and source Bash completions
echo 'eval "$(ctg autocomplete bash)"' >> "~/.bashrc"
source ~/.bashrc

# Generate and source Zsh completions
echo 'eval "$(ctg autocomplete zsh)"' >> "~/.zshrc"
source ~/.zshrc