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.agefile.
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.agefile: Usually treated as the source for decryption or the target for status/diff..cott.tomlfile: Metadata file. Most commands skip these directly as they are managed alongside the.cott.agefiles.
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.agefile 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.ageFile: Decrypts it into the corresponding plain-text file.- Directory: Recursively finds and decrypts all
.cott.agefiles. - 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.agefile 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.ageFile: Verifies the content checksum and recipient checksum against the metadata.- Directory: Recursively finds and verifies all
.cott.agefiles. - Decrypted File: Verifies the corresponding
.cott.agefile.
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.agefile 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.ageFile: Pulls the secret for this specific file if it has an upstream configured in its metadata.- Directory: Recursively pulls secrets for all
.cott.agefiles that have upstreams configured. - Decrypted File: Pulls for the corresponding
.cott.agefile.
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.ageFile: Pushes the secret for this specific file if it has an upstream configured in its metadata.- Directory: Recursively pushes secrets for all
.cott.agefiles that have upstreams configured. - Decrypted File: Pushes for the corresponding
.cott.agefile.
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