This repository holds shared tools for tbhb-dev work: commands that more than one repository or session uses and that no one repository owns. Every tool is a subcommand of one Python package, tbhbdev.
uv run tbhbdev <tool> [args...]tbhbdev with no arguments lists the tools. After the tool's name come that tool's own arguments, and it exits with its own codes.
To put tbhbdev on PATH, which the alpha harness and git hooks need, install it as a uv tool:
uv tool install git+https://github.com/tbhb-dev/toolsuv tool upgrade tbhbdev updates it.
secretreads and manages 1Password items through thetbhbdev.secretslibrary.hostmanages agent hosts from the operator's Mac.alphaholds the interim agent tools from ascm'stools/interim. They may change or go away without notice.
The values of one deployment come from the user config at $XDG_CONFIG_HOME/tbhbdev/config.toml, where XDG_CONFIG_HOME defaults to .config in the home directory. The package has no defaults for them, and no environment variable or plugin overrides them. When the file or a required key is missing, the command fails, and its error gives the file's path and the [table] key. Only the [workspace] table and [gitleaks] session_sockets are optional. tbhbdev reads the file only when a command first needs a value, so --help works without it.
[op]
account = "example.1password.com"
[connect]
server = "example-connect"
server_hosts = ["nas-a", "nas-b"]
token_prefix = "example-host"
[agent]
host_tag = "tag:example-agents"
[vaults]
all = "vault-all"
host_prefix = "vault-"
[gitleaks]
owners = ["example-org", "example-user"]
session_sockets = ["/tmp/example-socks/"]
[workspace]
root = "src"
repos = ["example-org/app"]Each [connect] server_hosts entry must be a DNS label of at most 63 characters. Letters and digits make up the label, and a hyphen may appear only between them. A bad entry fails with its position in the list, not its value.
That's the operator's file. On each agent host, tbhbdev host bootstrap installs a copy that adds [connect] endpoints, computed from the tailnet as Agent hosts describes.
Tools get secrets only from tbhbdev.secrets. It's the one module that talks to 1Password Connect or op at runtime, so a fallback for hosts off the tailnet can go there later. secrets.read("vault/item/field") returns one value, and also takes the op://vault/item/field form. secrets.Store covers item management: get, list, create, edit, delete, and move.
The library reads the Connect token from the current user's keychain: the agent keychain on macOS, and secret-tool on Linux. It tries the config's [connect] endpoints in order, the list bootstrap writes into an agent host's copy. A connection error or a 5xx answer moves it to the next endpoint, while a 4xx answer stops it.
Tests set the TBHBDEV_TEST_TOKEN environment variable, and the library then uses its value in place of the keychain token. Production code leaves it unset.
tbhbdev secret read REF prints exactly one value, for $(tbhbdev secret read ...). get prints the item's metadata and field labels, adding values with --reveal. create and edit read the fields as a JSON object of label to value from stdin or --file, never from the command line. delete asks for --yes.
tbhbdev host runs on the operator's Mac, which has 1Password Desktop and the op command-line tool. A tag marks the agent hosts on the tailnet. The config's [agent] host_tag names it. Each host gets a token for its own vault, [vaults] host_prefix followed by the host's name, and for [vaults] all, plus any the operator grants, stored in the agent account's keychain on that host. Every op call names the config's [op] account.
listprints each agent host on the tailnet with its OS, and marks this machine.bootstrap [--host H ... | --all] [--repair-acl] [VAULT ...]does a host's one-time, privileged setup, then runsupgradeon the same hosts. It sets up the Connect server and its vaults, thedevelgroup and theagentuser, workspace access, the agent keychain, the agent's copy of the config, and a current token. Bootstrap sends each host a copy of the operator's config on stdin and installs it as theagentuser'stbhbdev/config.toml, mode 0600 in a 0700 directory. It reportsokwhen the copy already matches andchangedotherwise, and never prints the file or a diff. Before the upgrade, it generates the private gitleaks config from the private repositories of each[gitleaks] ownersentry, the tailnet, the vaults, the Connect endpoints, the operator's account names and each path in the optional[gitleaks] session_sockets, and publishes it to theconfigfield of the itemgitleaks-privatein the vault[vaults] all, reportingokorchanged. A source that fails stops bootstrap before it writes anything. A second run doesn't change anything.upgrade [--host H ... | --all]brings a bootstrapped host up to date. It doesn't need a terminal, Touch ID,op, or a sudo password, so agents run it. Each step reportsok,changed, orfailed. It clones the workspace repositories as the agent's GitHub account and fast-forwards clean clones on their default branch. Then it runsmise installat the workspace root and checks that the host's token still reaches its vaults through Connect. Last, thegitleaks-configstep reads the private gitleaks config from that field through Connect with the host's own token. If the text extends.gitleaks.tomland defines thetbhb-private-canaryrule, the step installs it as.config/gitleaks/private.tomlin the agent's home and in the operator's: each a 0600 file in a 0700 directory, replaced atomically. Otherwise it fails without writing anything.grantandrevoketake[--host H ... | --all] VAULT ...and swap the host's token for one with the new vault set.revokerefuses to take a host's own vault or[vaults] allfrom it.statusreports each host's token name, vaults, and expiry, whether its keychain entry exists, whether its copy of the config matches yours, and whetherdevelhas its workspace access. It never prints the token.exec [--host H] [--operator] -- CMD ...runs a command on a host as theagentuser. With--operatorit runs the command as you, after a Touch ID check.
Bootstrap's copy sets [connect] endpoints to https://<host>.<suffix> for each of [connect] server_hosts, in order. Its suffix is the tailnet's MagicDNS suffix, the MagicDNSSuffix that tailscale status --json reports, so these commands need tailscale on the operator's Mac. That suffix stays out of every argument and message, and lives only in each host's 0600 copy. When tailscale is missing, fails, or doesn't report a suffix, bootstrap doesn't write a copy on any host. Any endpoints in the operator's own file give way to the computed list. The copy keeps values but drops comments and layout, so the file can contain only tables of strings and lists of strings. Grant and revoke check a new token against the same endpoints, and status compares each host's file with the same copy.
Without --host or --all, a command acts on this machine. Bootstrap, grant, and revoke create a new token when the host has none or its token covers other vaults. They also replace a token that expires within 14 days, or that the host's keychain lacks. The tool passes a token to other programs only on stdin. It never puts one in argv, a file, or a log.
Bootstrap, grant, and revoke start with a Touch ID check.
Bootstrap creates the group devel with group ID 33835 and the user agent with user ID 24368, with devel as its primary group, no password login, and a home of mode 0700. On macOS it uses dscl . and hides the user from the login window. On Linux it uses groupadd and useradd and locks the password. It never changes an existing account, and an account with other IDs stops that host with an error, as does a user or group ID that another name already uses. Bootstrap runs its root steps with sudo -n. When sudo needs a password, it runs them through ssh -t, so sudo asks once per host.
The user config's [workspace] table names the workspace with [workspace] root, a path below the operator's home, and repos, a list of owner/name. With one, bootstrap:
- lets
develtraverse each directory from the home down to the root - gives
develinheritable read, write, and create access on the root and everything below it - adds only missing entries, and reapplies the root's entries below the root only when the root lacked them or
--repair-aclasks - ends with a read and write probe as the agent
Bootstrap and upgrade skip the workspace when the config has no [workspace].
tbhbdev alpha holds the interim agent tools that the tbhb-dev repositories used to carry in tools/interim:
identityrecords which agent and model made each edit and writes theGenerated-bytrailers.guard-gh-pris a PreToolUse hook that blocksgh pr create,gh pr editandgh pr merge.prchecks.github/PULL_REQUEST_DRAFT.mdand opens or updates the pull request from it.mise run prruns it.mergesquash-merges an approved pull request.mise run mergeruns it.installinstalls the git hooks and the user-level harness hooks.
Run tbhbdev alpha install once on each host, from a clone of a tbhb-dev repository. It does two things:
- It installs that clone's git hooks into
$(git rev-parse --git-common-dir)/hooks, which every worktree of the clone shares. prek runs pre-commit and pre-push, and three shims runtbhbdev alpha identityfor prepare-commit-msg, commit-msg and post-commit. Withouttbhbdevon PATH, the shims block a commit in a tbhb-dev repository and do nothing elsewhere. - It merges the hooks into each harness's user-level config:
settings.jsonin$CLAUDE_CONFIG_DIRor.claudein$HOMEfor Claude Code,hooks.jsonin$CODEX_HOMEor.codexin$HOMEfor Codex, and.gemini/config/hooks.jsonin$HOMEfor Antigravity.
Each hook command it writes ends in # tbhbdev alpha install. A rerun replaces only those entries, and keeps everything else in place. Before it changes a config, it saves the old one beside it as <name>.tbhbdev-backup.<time>. --dry-run prints the changes without writing them, and --harness claude|codex|antigravity limits the merge to one harness. Codex asks you to trust new hooks with /hooks.
The hooks are user level, and the harness runs them in every repository. Each one first checks that the repository's origin is a tbhb-dev repository on GitHub, and does nothing otherwise. Claude Code's SessionStart hook runs tbhbdev alpha install --hook, which reinstalls the git hooks of the worktree the session starts in.
The alpha commands write each worktree's state under $(git rev-parse --git-dir)/tbhb-alpha, and each worktree has its own pull request draft, so sessions in two worktrees of one clone don't overwrite each other.
Another installed package can add commands and config through an entry point in the tbhbdev.plugins group. The entry point resolves to a module or other object with either or both of:
TOOLS: dict[str, Tool], extra commands. A name that's already taken is an error, never a silent override.config(name: str) -> str | None, the text of a named config file, orNone. The package doesn't include a config file of its own, so there's no default for a plugin's file to replace. The built-in tools read deployment values from the user config alone, never from a plugin.
Plugins load on first use. When one fails to import, tbhbdev names it and the error on stderr, and the built-in commands keep working. To run tbhbdev with a plugin, install both in one environment:
uvx --from git+https://github.com/tbhb-dev/tools --with <plugin package> tbhbdev <tool> [args...]src/tbhbdev/is the package. Each tool is a module there, registered incli.py.src/tbhbdev/alpha/is thealphapackage, one module for each alpha command.tests/holds the tests, onetest_<tool>.pyfor each tool andtest_alpha_<command>.pyfor each alpha command.
Bazel runs every gate from tbhb-dev/rules: bazelisk test //.... See AGENTS.md for the setup and the workflow.