No description
  • Go 96.4%
  • Just 3.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-25 11:26:09 +00:00
cmd/secret feat: improve prefix handling 2026-07-16 14:51:50 +02:00
internal feat: improve prefix handling 2026-07-16 14:51:50 +02:00
.gitignore initial commit 2026-07-16 10:14:03 +02:00
.gitlab-ci.yml Configure Secret Detection in .gitlab-ci.yml, creating this file if it does not already exist 2026-07-16 08:09:37 +00:00
.golangci.yml initial commit 2026-07-16 10:14:03 +02:00
go.mod feat: improve prefix handling 2026-07-16 14:51:50 +02:00
go.sum feat: implement secret handling 2026-07-16 10:14:58 +02:00
justfile initial commit 2026-07-16 10:14:03 +02:00
README.md docs: add license to README.md 2026-07-25 11:26:09 +00:00

secrets

secrets is a small Go CLI for managing credentials in the macOS Keychain. The compiled binary is called secret (singular) — one credential per invocation.

It is the successor of a set of shell helpers (secret-set, secret-get, secret-list, secret-delete) built on top of the security(1) tool that ships with macOS.

Credential names are opaque strings you choose. The tool imposes no naming scheme; pick whatever fits your workflow.

Platform: macOS only. Other platforms have no Keychain, so the tool has nothing meaningful to do there.


Status

This repository is being built up in small, reviewable steps.

  • Step #1: basic CLI scaffolding — commands, help, unit tests, build tooling. Done.
  • Step #2 (current): macOS Keychain backend wired to every subcommand; interactive masked prompt for set; distinct exit codes; drop-in compatible with the original shell helpers. Done.
  • Step #3 (next): richer list output (JSON, grouping by authority), shell completions, release automation.

Installation

git clone https://gitlab.com/doertydoerk/secrets.git
cd secrets
just install       # installs `secret` into $GOBIN (or $GOPATH/bin)

Or, once the module is publicly available:

go install gitlab.com/doertydoerk/secrets/cmd/secret@latest

Local build

just build         # produces ./secret in the repo root
./secret --version

Usage

secret set <name>            create or update a credential (interactive prompt)
secret get <name>            print a credential value to stdout
secret list                  list every managed credential
secret list -p <prefix>      list only names starting with <prefix>
secret delete <name>         delete a credential (alias: rm)
secret help [command]        general help or help for a specific command
secret --version             print version, commit and build date

Names are opaque strings — the tool never rewrites them. secret set foo stores the credential exactly as foo; secret get foo reads it back.

Entering the value for set

The credential value is never taken from the command line, so it cannot leak into ps, shell history, or scrollback.

  • Interactive (stdin is a terminal): you are prompted for the value; each character is echoed as *.

    $ secret set github_com_personal
    Value: ****************
    $
    
  • Piped (stdin is not a terminal): the value is read from stdin. Exactly one trailing \n (or \r\n) is stripped — everything else is preserved byte-for-byte.

    printf 'sk-xxxxxxxxxxxxxxxx' | secret set openai_chatbot_backend
    op read op://vault/item/password | secret set aws_prod_terraform
    

Examples

# Create / update
secret set github_com_personal          # prompts for value
printf 'sk-xxx' | secret set openai_personal

# Retrieve (pipe-friendly; ends with a newline)
export GITHUB_TOKEN="$(secret get github_com_personal)"

# List everything managed by this tool
secret list

# List only entries whose name starts with a given prefix
secret list -p github_

# Delete
secret delete github_com_personal
secret rm openai_chatbot_local

Exit codes

Code Meaning
0 success
1 generic error (validation, unexpected I/O)
2 credential not found (get / delete)

How it maps to the Keychain

Under the hood every credential is a generic password item with:

  • account (-a) = secret (fixed — required by security(1); used consistently on set/get/delete for drop-in compatibility with the original shell helpers).
  • service (-s) = the credential name, verbatim as you provided it (e.g. github_com_personal).
  • password (-w) = the secret value.

secret list is different: it reads every service name in the login keychain (the Go equivalent of security dump-keychain … | awk … | sort -u), regardless of account, so entries created by other tools also appear. Use -p <prefix> to narrow the output.

That is the same schema used by the original shell helpers, which means the Go tool is drop-in compatible with credentials created by the shell version and vice versa.

The concrete Keychain calls are implemented in internal/keychain/keychain_store.go as thin wrappers over security(1). Entries created here are indistinguishable from those created by the original shell helpers.


Development

Layout

secrets/
├── cmd/
│   └── secret/          # tiny main package; entry point
└── internal/
    ├── cli/             # cobra command tree (root, set, get, list, delete, help)
    └── keychain/        # Store interface + naming helpers (backend impl comes later)

internal/ keeps the CLI and backend unimportable by third parties — the CLI is a product surface, not a library.

Prerequisites

  • Go 1.23+
  • just — brew install just
  • golangci-lint — brew install golangci-lint
  • gotestsum — brew install gotestsum (per-package ∅ / ✓ / ✗ summary output, prints full failure detail on failing tests)

Common tasks

just             # list available recipes
just all         # lint + test + build
just build       # compile ./secret
just test        # gotestsum ./... (per-package summary, cache disabled)
just test [pkg]  # gotestsum on a specific package, e.g. just test ./internal/cli
just test-race   # gotestsum ./... -race
just cover       # gotestsum with coverage + HTML report
just lint        # golangci-lint run ./...
just lint-fix    # golangci-lint run ./... --fix
just tidy        # go mod tidy
just clean       # remove ./secret and coverage.out

Version injection

version, commit and date are baked into the binary via -ldflags by the justfile. When you build with a plain go build/go install, they default to dev/none/unknown.

Values come from git at build time:

  • version = git describe --tags --always --dirty
  • commit = git rev-parse --short HEAD
  • date = current UTC timestamp

Release flow

The three variables populate automatically — the order matters:

  1. Commit all pending changes (working tree must be clean).
  2. Tag the commit: git tag -a v0.2.0 -m "v0.2.0".
  3. Build: just build (or just install).

Result: secret -v prints v0.2.0 (commit abc1234, built …).

Building at other times still self-identifies honestly:

Working tree state at build secret -v shows
Clean, HEAD is at a tag v0.2.0 (…)
Clean, no tag on HEAD abc1234 (…)
Uncommitted changes present abc1234-dirty (…)
Built without just (no -ldflags) dev (commit none, …)

Sanity-check before shipping a tagged build:

git status                              # must be clean
git describe --tags --always --dirty    # must print exactly the tag
just build
./secret -v                             # must show the tag

Design notes

  • Consumer-defined store interface. The store interface lives in internal/cli (where it is consumed), not in internal/keychain (where the concrete type lives). This keeps the keychain package free of abstractions it doesn't need, while still letting tests inject a fake.
  • context.Context on every backend call, ready for cancellation and timeouts once we ever need them.
  • RunE, not Run, so cobra propagates errors and the process exits non-zero on failure.
  • Testify (assert + require) for readable, table-driven tests.
  • SilenceUsage / SilenceErrors on the root command so error output stays a single clean line prefixed with secret:.

Roadmap

  • Step #3 — richer list output (grouping by authority, JSON).
  • Step #4 — dynamic completion of credential names for get / delete.
  • Step #5 — release automation (goreleaser, Homebrew tap).

License

MIT.