Summary
- Tallyfy CLI v0.1.0 copies its settings model from Claude Code - six configuration scopes merge in a fixed order (defaults, user, project, local, flags, managed) because Anthropic already argued those semantics through in public
- Scalar keys override, list keys merge - one default org wins by precedence, while permission rules, hooks, and MCP servers accumulate across every scope.
tallyfy config list --show-sourcesnames the file behind each value - What runs without asking? Read verbs. Writes prompt a human, deny beats allow, and scripts must pass
-yfor destructive actions while--no-inputturns any would-be prompt into a loud failure so CI never hangs waiting - Cloned repos get zero trust by default - project hooks stay inert until you run
tallyfy trust, and tokens live in the OS keychain through go-keyring with an AES-256-GCM file fallback. Install:brew install tallyfy/tap/tallyfy
The Tallyfy CLI shipped this week. Version 0.1.0, one Go binary, installed with brew install tallyfy/tap/tallyfy. It launches processes, completes tasks, exports blueprints as JSON you can commit to git, and blocks CI pipelines until a human approves a step. Underneath all of that sits a configuration system we didn’t invent. We modeled it on the Claude Code configuration architecture, the layered settings design Anthropic ships with its coding agent, because we use Claude Code daily and its config model answers the questions every serious CLI eventually faces: where a setting comes from, who wins when two files disagree, and what’s allowed to run when nobody is watching.
The CLI is our third door into the same engine. Developers embedding Tallyfy reach for the REST API; AI agents holding a conversation use the MCP server. Our new command line covers the lane between those two: cron jobs, CI runners, and shell scripts that need workflow automation to behave identically on run one and run one thousand. Agents do their best work when each step is a small, checkable unit, and a deterministic command line is how those units end up in a pipeline.
This is the build memoir. What we copied, what we bent, and what v0.1.0 leaves out on purpose.
Why copy a coding agent’s settings model?
Because configuration is where CLIs rot, and this is the first model we’ve used that resists the rot. Most tools grow config the way houses grow extension cords: a dotfile here, an env var there, a flag that overrides one of them but nobody remembers which. Claude Code sat down and specified the whole ladder instead, and the result is laid out in Claude Code’s settings reference.
The short version, for anyone who hasn’t lived in it. Claude Code resolves its behavior from layered JSON settings files. A user file at ~/.claude/settings.json applies across every project on a machine. The project file at .claude/settings.json is committed to the repository and shared with every collaborator. Beside it sits .claude/settings.local.json, gitignored and personal. Managed settings, deployed by an organization to system directories, sit above them all.
Precedence is where it gets interesting. When the same scalar key appears in several files, it runs managed policy first, then command-line arguments, then local, then project, then user. Permission rules are the documented exception: they merge across scopes rather than override, so a deny rule written in any file keeps applying regardless of what a lower scope allows. Allow rules in the committed project file take effect only after the workspace is trusted. Rules in the personal local file apply immediately, no trust step required.
Dry as toast, and also a complete answer to config drift. We read that page and felt no urge to reinvent the wheel.
Fair enough, a workflow CLI is not a coding agent. But look at what the two have in common: individual preferences, a team’s shared conventions checked into a repo, an unattended runner somewhere, and an IT department that wants the last word. Every one of those is an organizational problem, and Tallyfy is nothing but organizational problems wearing software.
One thing we did change is the words. Tallyfy’s UI says blueprint and process. The API, for reasons that made sense sort of a decade ago, says checklist and run. The CLI speaks the UI’s language and translates underneath, so tallyfy blueprint export fetches what the wire calls a checklist and nobody scripting it ever needs to know.
Did a one-week-old tool need this much settings machinery? Probably not on day one. But permission models are the kind of thing you design in early or bolt on badly later, and bolting one on after people have scripts in production is rework nobody volunteers for twice.
Six scopes, one merge rule
Settings in the Tallyfy CLI merge across six scopes, lowest to highest: built-in defaults, your user file, the project’s committed .tallyfy/settings.json, a personal .tallyfy/settings.local.json, command-line flags, then managed policy from your org. Scalar keys override, and the higher scope wins. List keys do the opposite: permissions, hooks, and MCP servers merge across every scope instead of replacing each other. When a value surprises you, tallyfy config list --show-sources prints which file set each effective setting, which turns a twenty-minute mystery into a five-second lookup.
The counterintuitive part is that both behaviors are correct, and neither would survive alone. If lists overrode, a project could erase an org’s deny rules by shipping its own permissions array in a committed file. If scalars merged, you could never change your default output format without archaeology across three files. Override for values. Union for guardrails.
We keep saying we copied Claude Code. That’s not quite right: we copied the questions its design answers, then re-derived the answers for a workflow tool. Claude Code defines four settings files; we ended up with six layers because compiled-in defaults and one-off flags deserve named rungs on the same ladder rather than special cases floating around it. There’s an escape hatch too: --settings accepts a path to a JSON file or an inline JSON object for a single run, which is a bit of a kludge and exactly what you want in a CI matrix.
The active organization follows the same thinking. tallyfy org use saves your org to state.json, the --org flag overrides it for one command, and a managed forceOrg overrides everybody. An org that wants a build runner locked to one workspace gets it in one managed key, and no local file can argue.
Layered config means layered confusion, so the CLI ships a stethoscope. tallyfy doctor runs read-only checks across the stack: configuration, credential source, API reachability, active org, permission-rule validity, and the credential backend. The help text says to run it first whenever a command misbehaves, which is also just the advice we’d give in a support thread.
Treat every cloned repository as hostile
The permission engine and the trust gate exist for the same reason: a CLI that can change production workflows will eventually run somewhere no human is watching.
Every permission rule names a resource and a verb, so a rule reads like process(launch), blueprint(export), or task(complete). Claude Code writes the same shape as Bash(npm run lint) or Read(~/.zshrc). Read-only verbs are allowed by default: listing tasks, showing a process, exporting a blueprint, none of it prompts. Anything that writes asks a human first.
Deny beats allow, from any scope, every time.
That posture is lifted from Claude Code’s permission rules, which run file reads without approval, stop before shell commands and file edits, and evaluate deny before ask before allow. Why does a workflow tool need that rigor? Because the blast radius isn’t a laptop. A messy script against a workflow API can complete tasks that real people were mid-way through, and undoing that is a morning of apologies.
Scripts change the posture, deliberately. -y or --yes assumes yes for prompts and is required for destructive actions in scripts, so the dangerous path has to be chosen in writing. --no-input never prompts and fails instead, and the CLI auto-detects a missing TTY and applies it for you. --dry-run prints the API calls a command would make and executes none of them, which is how you rehearse a bulk operation against production without touching it.
Something we learned the hard way from years of cron scripts against our own API: a script that prompts is a script that hangs. At 3 a.m. On a Saturday. The --no-input behavior exists so that failure shows up in a log instead of a frozen terminal nobody is watching. The same script-first thinking is why tallyfy task wait blocks until a named task completes, turning a human approval into a CI gate, and why tallyfy api maps errors to stable exit codes, 5 for not-found and 7 for validation, that a shell script can branch on.
Then there’s the poisoned repository problem, and here we took the Claude Code lesson whole. A committed .tallyfy/settings.json can define hooks, and hooks execute commands. So project- and local-scope hooks run only in a workspace you’ve marked trusted with tallyfy trust. Clone a stranger’s repo, run one innocuous command inside it, and nothing in that repo’s config can execute anything on your machine. tallyfy trust status reports where you stand; tallyfy trust remove revokes it. Claude Code draws the equivalent line by making a committed project file’s allow rules wait for workspace trust while your own local rules apply immediately. The attack it prevents is quiet and clunky to explain after the fact, which is exactly why the gate has to sit in front of it.
Where tokens live, and what v0.1.0 leaves out
tallyfy login opens your Tallyfy settings page in a browser and asks you to paste a token. That’s the whole flow. The token gets validated against GET /me, so a typo fails at login rather than three commands later, and then it’s stored in the OS keychain. --no-browser just prints the URL for SSH sessions, and echo "$TOKEN" | tallyfy login --stdin covers provisioning scripts.
Saved tokens are only the last resort in a fixed resolution order: the --api-key flag, then the TALLYFY_API_TOKEN environment variable, then an auth.apiKeyHelper script, then whatever tallyfy login stored. First hit wins. The helper script is another Claude Code borrow: point the setting at any executable that prints a token and the CLI calls it, which lets a secrets manager mint short-lived credentials instead of parking a long-lived one anywhere.
For storage we picked go-keyring, Zalando’s Go library that wraps the Keychain on macOS, Credential Manager on Windows, and the Secret Service D-Bus interface, provided by GNOME Keyring, on Linux. Headless Linux boxes often run no Secret Service at all, so when a keychain isn’t available the token goes into an AES-256-GCM encrypted file instead. GCM is authenticated encryption, specified in NIST SP 800-38D back in 2007, so a tampered file fails loudly rather than decrypting into garbage. An encrypted file sitting next to its own user is a fallback, not a fortress, and we’d rather say so than imply otherwise. Either way, --verbose logs every request with tokens redacted, so a debug trace pasted into a ticket doesn’t leak the secret.
The CLI also feeds the AI lane it complements. tallyfy mcp add writes server entries to .tallyfy/mcp.json so AI clients can discover the Tallyfy MCP server, and tallyfy mcp snippet prints the exact config block for a specific client instead of making you transcribe it from docs.
Now the gaps, because a launch post that hides them is marketing.
No browser OAuth yet. The right flow is authorization code with PKCE, and v0.1.0 doesn’t have it. Could we have held the release until it did? Sure, and developers would have spent another month with no CLI at all. The paste flow works on every terminal we tried, including SSH sessions with no browser anywhere in sight, so it shipped first.
The binaries aren’t code-signed. tallyfy update verifies every download against release checksums before atomically replacing the running executable, and Homebrew-managed installs are pointed at brew upgrade instead, but signing and notarization are still ahead of us. Greenfield projects get to sequence these things; we chose checksums first.
No webhook commands, and not from laziness. The API has no standalone webhook-subscription surface to wrap, because we merged webhook subscriptions into the watching system, a call we wrote up separately. A CLI verb over an endpoint that doesn’t exist would be a lie with tab completion.
The takeaway that surprised us is a little deflating for anyone who enjoys design work. That configuration layer, the part we copied, is the part we trust most. What we invented ourselves, the resource verbs, the vocabulary aliasing, the org gating, is where the bug reports will come from. A well-argued architecture is a list of fights someone else already had, and Anthropic published theirs. Our job was picking which fights were actually ours: workflows, organizations, approvals. The rest we borrowed without embarrassment. The source is on GitHub, the CLI docs cover every flag, and if you find a place where we bent the model too far, that’s what issues are for.