Changelog¶
Changelog¶
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.9.0] - 2026-04-18¶
Sprint 9 — Sprint 8 Carry-out + Pyright Gate. Five stories totalling 7 points. Completes the Sprint 8 spawned backlog (HMB-S039/S040/S041), installs the pyright basic-mode quality gate per DRN-077 (HMB-S037), and codifies the scrub-skills workflow at the vault-policy level (HMB-S038) so OWB Sprint 31+ inherits the sequencing contract.
Added¶
hmb rotate-key --rule <path_regex>. When.sops.yamlhas more than onecreation_rulewith anagerecipient,--ruleselects which one to rotate. Value is a regular expression matched against each rule'spath_regex; must resolve to exactly one rule or the command aborts non-zero with the list of matches. Single-rule setups are unaffected. Established by HMB-S040.- Pyright basic-mode gate.
pyright>=1.1,<2.0in[dev]extra;pyrightconfig.jsonscoped tosrc/(matches mypy); CI step added aftermypyin.github/workflows/ci.yml. Error budget is zero; the three src/ issues surfaced by the HMB-S036 spike are suppressed inline with scoped# pyright: ignore[<rule>]directives naming each rule. Established by HMB-S037. .pre-commit-config.yamlwith opt-in local hooks (ruff check, ruff format, mypy, pyright) running through the project venv viauv runso local and CI cannot drift. Opt in withuv run pre-commit install. Part of HMB-S037.
Changed¶
hmb initexits non-zero on sops encrypt failure. Previously warned and continued, leaving a misleading plaintext.secrets.enc.yamlon disk; now unlinks the plaintext file and raises aClickException. Behavior change for consumer scripts that relied on the warn-and-continue path — the minor version bump covers the semver break for pre-1.0 releases. Established by HMB-S039.hmb rotate-keyno longer silently collapses multi-rule.sops.yaml. Previously overwrote everycreation_rules[].agefield with the new public key, destroying multi-recipient configurations without warning. Now aborts non-zero on ambiguity (with a list of each rule'spath_regexand currentage) and requires--ruleto select one rule. Established by HMB-S040.BackendRouter.list_keyspartial-failure notice is now emitted viawarnings.warn(UserWarning)instead of directprint(..., file=sys.stderr). Library consumers can suppress or capture through standardwarningsmachinery. Part of HMB-S041 (LOW-4).- CONTRIBUTING.md lists pyright alongside ruff/mypy/pytest in the quality-gate section; documents the pre-commit opt-in. Part of HMB-S037.
Security¶
- Atomic age keys file creation.
_ensure_age_key()now creates the age private-key file viaos.open(O_CREAT|O_WRONLY|O_EXCL, 0o600)+os.write. Previouslywrite_text()respected the process umask (typically 0o644 or 0o664) and a trailingchmod(0o600)narrowed the mode, leaving a race window during which the age private key was world-readable on a shared multi-user host. Closes HMB-S034 HIGH-3 carry-out. Established by HMB-S039. - Audit directory mode warning.
_ensure_audit_dirnow emits a one-timeUserWarningif the audit directory pre-exists with a mode wider than 0o700. The mode is not narrowed — the "do not chmod down on every write" invariant is preserved. Part of HMB-S041 (SEC-LOW-3). - BitwardenBackend session invalidation.
_raise_friendlynow clearsself._sessionwhenbwreports a locked vault, so the next operation re-runsunlock_commandinstead of retrying with a dead session token. Part of HMB-S041 (SEC-LOW-2). User-Agent: himitsubako/<version>on OAuth device-flow POSTs. Improves Google-side telemetry distinguishability for incident response. Part of HMB-S041 (SEC-LOW-4).
Fixed¶
_default_postdirect test coverage. Previously only tested indirectly via the flow-level tests that injected a fake transport. New tests exercise the HTTPError-with-JSON-body path (returns dict, not raises), the URLError wrap (BackendError with endpoint named, no urllib traceback leaked), and the User-Agent header. Part of HMB-S041 (LOW-3).HimitsubakoSettingsSourcecache lifetime documented._resolve_backendcaches the backend onself._backendfor the source's lifetime; if on-disk config changes mid-process, callers must construct a new source. Documented in the method docstring. Part of HMB-S041 (MED-6).
Policy / governance (vault-only, not in this repo)¶
- Scrub-skills section in
Obsidian/code/development-process.md. Names/simplify,/code-review, and/refactor-cleanas concrete steps in the SDLC, with strict ordering, per-skill blocking rules, and a sprint-close record-keeping requirement. Quick reference atObsidian/code/scrub-skills-quick-reference.md. OWB Sprint 31 inherits via OWB-S140 (absorb story filed). Established by HMB-S038.
[0.8.0] - 2026-04-18¶
Sprint 8 — Code Quality Scrub. Four-pass scrub toolchain run as the
reference implementation for OWB Sprint 31 to reuse:
/simplify → /code-review → /refactor-clean → pyright triage spike.
Eight points across four stories. Net effect: tighter code, hardened
subprocess + redaction surface, one public API removal, and pyright
basic-mode adoption per DRN-073.
Added¶
docs/public-api-changes.md. Canonical record of public-API changes between releases. Downstream consumers (open-workspace-builder, home-ops, and internal consumers) read this file to absorb breaking changes in their next sprint. Format: removed / renamed / signature- changed sections, per release. Established by HMB-S035.pyrightconfig.jsonat repo root. Basic mode per DRN-073 and DRN-077;venvPath/venvkeys configured so pyright resolves project dependencies from.venv. ThreereportUnknown*rules suppressed to prevent third-party stub gaps from drowning real signal. CI integration is deferred to HMB-S037 (Sprint 9). Established by HMB-S036.docs/pyright-basic-inventory.jsonanddocs/pyright-strict-inventory.json. Snapshot inventories from the HMB-S036 spike — basic 25 errors, strict 1161 errors. Used to size follow-up annotation work in Sprint 9-10.- Redaction patterns for Google OAuth refresh tokens (
1//<...>) and age private keys (AGE-SECRET-KEY-1<...>)._redaction.pynow runs three patterns in sequence; previously only matched generic base64-shaped opaque tokens (40+ chars). Per HMB-S034 finding SEC-MED-2.
Changed¶
bitwarden.unlock_commandis now parsed withshlex.split()and executed withshell=False. Shell metacharacters in the config value no longer trigger command execution;pass show <path>-shape commands continue to work; pipelines need a wrapper script. Closes the command injection vector documented as T-022 residual risk. Per HMB-S034 finding SEC-HIGH-3 / HIGH-2.hmb rotate-keywrites.sops.yamlatomically via tempfile +os.replace. A SIGKILL or disk-full mid-write previously left the config truncated; now leaves either the old or new content, never partial. Per HMB-S034 finding HIGH-1.hmb rotate-keyandhmb inithonorHIMITSUBAKO_SOPS_BINwhen invokingsops. Previously hardcoded the bare"sops"PATH name, bypassing the binary-resolution mitigation already in place forSopsBackend. Per HMB-S034 finding SEC-HIGH-2.- 30s subprocess timeouts added to
hmb rotate-key(sops updatekeys),hmb init(age-keygen,sops --encrypt). Previously no timeout; a hung subprocess could block the CLI indefinitely. Per HMB-S034 finding MED-3. - SOPS stderr is redacted before being interpolated into
BackendError.detail. Previously the raw subprocess stderr passed through to callers and the audit log. Per HMB-S034 finding SEC-HIGH-1 (decrypt + encrypt) and SEC-MED-4 (sops updatekeys). - Google OAuth
error_descriptionis redacted before being included in error messages. Google'serror_descriptionfor some codes can echo back submitted credentials. Per HMB-S034 finding MED-1. _default_postno longer embeds payload bytes inBackendError.detailon non-JSON OAuth response. Previously includedpayload[:200]!rwhich could leak server-echoed request bytes (includingclient_secret). Now reports byte count only. Per HMB-S034 findings SEC-MED-1 / MED-2.hmb rotate-key --new-keynow usesclick.Path(exists=True, dir_okay=False, path_type=Path)so click handles the existence check at parse time. Exit code for a missing file changes from 1 (ClickException) to 2 (click parse error). Per HMB-S034 finding LOW-5._read_rotation_valuenow strips\r\n, not just\n. A Windows-encoded credential file would have silently included a trailing carriage return in the stored secret. Per HMB-S034 finding LOW-1 / SEC-LOW-1.
Fixed¶
- Explicit
import urllib.erroringoogle_oauth_rotate.py. Previously relied onurllib.requestindirectly importingurllib.error; worked at runtime but a urllib internals change could have broken it silently. Surfaced by HMB-S036 pyright spike. set_secretnarrowing.value: str | Noneafter the google-oauth / value-from-prompt branches is non-None at runtime; anassertmakes that visible to type checkers. Surfaced by HMB-S036 pyright spike.generate_envrc()and_managed_block()no longer duplicate the sops eval line.generate_envrc()now delegates to_managed_block()instead of inlining the same construction. Output byte-identical. Per HMB-S033 finding Q6._rotate_google_oauth(backend_name=)parameter removed. The parameter was alwaystarget.backend_nameat the caller; now derived inside the function. Per HMB-S033 finding Q3.
Removed¶
himitsubako.backends.google_oauth.GoogleOAuthBackend.credential_nameproperty — removed without deprecation. Speculative API added in HMB-S030 with no callers in any internal repo. Documented indocs/public-api-changes.md. Per HMB-S035 finding B1.himitsubako.cli.init._build_envrc— internal forwarder for v0.1.0 tests that no longer exist. Single internal caller now usesdirenv.generate_envrcdirectly. Per HMB-S035 finding A1.HimitsubakoSettingsSource.prepare_field_value— internal no-op override that matched thepydantic_settings.PydanticBaseSettingsSourcebase-class default for non-complex fields (we hardcodevalue_is_complex=False). Per HMB-S035 finding A3.
Test infrastructure¶
- New
tests/test_redaction.py— 14 tests covering generic, Google refresh token, and age private key patterns + combined-input cases. - 3 new tests in
tests/test_bitwarden_backend.py— shell-injection regressions for theunlock_commandswitch fromshell=Truetoshlex.split() + shell=False. - 3 new tests in
tests/test_cli_rotate.py—HIMITSUBAKO_SOPS_BINhonored,.sops.yamlatomic rollback on simulated disk-full,\r\nstrip from a Windows-encoded credential file. test_concurrent_writes_are_atomic(audit log) switched frommultiprocessing.get_context("fork")tospawn. fork is unsafe in multithreaded processes and Python 3.12+ deprecates the macOS default. Worker callable moved to module scope to be picklable. Per HMB-S034 finding MED-5.
Stories¶
| Story | Title | Pts | PR |
|---|---|---|---|
| HMB-S033 | /simplify skill scrub |
2 | #2 |
| HMB-S034 | /code-review skill scrub |
2 | #3 |
| HMB-S035 | /refactor-clean scrub + public-API changelog |
2 | #4 |
| HMB-S036 | Pyright strict triage spike + DRN-077 | 2 | #5 |
Spawned backlog¶
Per HMB-S034 finding triage, three follow-up stories filed:
- HMB-S039 — Fix age private-key file mode race in
hmb init(HIGH-3, ~1pt). - HMB-S040 —
hmb rotate-keymulti-rule.sops.yamlsafety (MED-4, ~1pt). - HMB-S041 — Code-review tech-debt bundle (MED-6, LOW-2/3/4, SEC-LOW-2/3/4, ~2pt).
HMB-S037 (pyright pre-commit hook install) sized at 1pt based on HMB-S036 spike data.
Test count¶
- Sprint baseline: 254 (v0.7.0)
- Sprint close: 274 (+20)
- Coverage: 85.84% → 85.83% (no meaningful change)
Decisions¶
- DRN-077 — himitsubako adopts pyright in basic mode per DRN-073.
[0.7.0] - 2026-04-14¶
Sprint 7 exits maintenance mode with Google OAuth support and a P1 SOPS
bug fix. Three stories, eight points. New google-oauth composite
backend type, hmb rotate plugin for OAuth refresh tokens with device
flow and browser flow, and SOPS subprocess env propagation for non-default
key paths.
Added¶
- Google OAuth credential backend (HMB-S030). New
google-oauthcredential type in.himitsubako.yamlgroups the three Google OAuth secrets (client_id, client_secret, refresh_token) as one logical credential. Delegates storage to any existing backend (sops,env,keychain,bitwarden-cli). Python API exposeshimitsubako.get_google_credentials(key)which returns a livegoogle.oauth2.credentials.Credentialsobject ready forgoogle-api-python-clientconsumers. CLI:hmb get <cred>emits JSON with all three fields,hmb set <cred>prompts for each field separately. Individual constituent keys remain accessible via the normalhmb get <key>/ Pythonget(key)paths. Optional dependenciesgoogle-auth>=2.49.0,<3.0,!=2.49.2andgoogle-auth-oauthlib>=1.3.1,<2.0under the new[google]extras. hmb rotateGoogle OAuth plugin (HMB-S032). When the resolved target is a google-oauth credential,hmb rotateruns an OAuth authorization flow to obtain a fresh refresh token and writes it back to the configured storage backend automatically. Two modes: device flow (default) prints a verification URL and user code that works over SSH, in containers, and any environment without a browser;--browserflag usesInstalledAppFlowwith a localhost callback for desktop use. New audit logmethodfield records which flow was used. Device flow errors include a remediation hint pointing at--browserwhen the GCP OAuth client is not configured for device flow.
Fixed¶
- SopsBackend now propagates
age_identityandconfig_file(HMB-S031)..himitsubako.yaml'ssops.age_identityis now passed to the sops subprocess asSOPS_AGE_KEY_FILEenv var, and a newsops.config_fileoption is passed as--config. Without this, consumers with non-default key paths or.sops.yamllocations hit "identity did not match any of the recipients" errors even with a correctly populated config. Theage_identityfield is nowstr | Nonewith a default ofNone(SOPS uses its own default resolution). Tilde expansion applied uniformly tosecrets_file,age_identity, andconfig_file. Unblocks home-ops HOP-S106 secrets migration.
Changed¶
write_audit_entrygains an optionalmethodfield. JSONL schema evolution is backward-compatible; legacy entries omit the field. Used today byhmb rotateto recordmethod: deviceormethod: browseron google-oauth rotations.
[0.6.0] - 2026-04-11¶
Sprint 6 clears the remaining backlog with docs polish and a CI safety net. Three stories, five points, no library code changes. The backlog is empty after this sprint; the project enters maintenance mode.
Added¶
- TestPyPI dry-run workflow (HMB-S027). New
.github/workflows/testpypi.ymltriggered on pre-release tags (v*-rc*). Mirrors the production release workflow but publishes to TestPyPI, giving every release a low-risk rehearsal before the real PyPI publish. Same SHA-pinned actions, Trusted Publishers OIDC, PEP 740 attestations, and required-reviewer gate. Tag patterns are mutually exclusive withrelease.yml. Motivated by the v0.3.0 → v0.3.1 incident where a Docker image pull failure was only caught on the production tag.
Changed¶
- Attestation docs use version placeholders (HMB-S028). All
hardcoded
v0.4.0references indocs/security/attestations.mdverification commands replaced with<VERSION>placeholders. An admonition callout explains the substitution convention and thevprefix difference between git refs and pip arguments. - Docs site aligned with OWB brand identity (HMB-S029). Palette
switched from stock indigo to the originalrgsec gold (#D4A017) /
royal blue (#3A5BA0) scheme with dark-first slate mode. System
sans-serif font stack (
font: false), profile-avatar logo and favicon, gold glow effects on navigation, and custom admonition styling. Brand CSS ported from open-workspace-builder; layout classes (hero, feature cards, buttons) excluded.
[0.5.0] - 2026-04-11¶
Sprint 5 expands distribution reach beyond PyPI and closes the macOS CI gap. Three stories, seven points, no library code changes.
Added¶
- macOS runner for keychain integration tests (HMB-S024). The CI
workflow now includes a
test-macosjob onmacos-latest(arm64) that exercises the keychain backend against a real macOS login Keychain. Uses SHA256-verified sops + age darwin-arm64 binaries, single Python 3.13 to control 10x macOS runner cost, and a runtime arch assertion to fail clearly if the runner image shifts to x86_64. Alluses:directives are SHA-pinned per supply-chain policy. - conda-forge recipe (HMB-S025). A
recipe.yaml(v1 format, CEP 13) for conda-forge was written and submitted as conda-forge/staged-recipes#32938. Uses PyPI sdist as source,noarch: python, and lists all five runtime dependencies. Merge is controlled by conda-forge maintainers. Local copy atconda/recipe.yaml. - Homebrew tap (HMB-S026). A new repo
originalrgsec/homebrew-tap
provides
brew install originalrgsec/tap/himitsubako. The formula passesbrew audit --newandbrew testclean. Includes all transitive Python dependencies as resource stanzas, depends on Rust for building pydantic-core from sdist, and recommendssops+age.
[0.4.0] - 2026-04-11¶
Sprint 4 closes the loops still open after the v0.3.1 PyPI publish: a credential rotation command with an append-only audit trail, a live published docs site at originalrgsec.github.io/himitsubako, and PEP 740 / Sigstore provenance attestations on every release artifact. Three stories, seven points, no new runtime dependencies.
Added¶
hmb rotate <credential>— credential value rotation with audit log (HMB-S021). A new CLI command that rotates a single credential's value throughBackendRouterand appends a JSON Lines entry to~/.himitsubako/audit.log. Distinct fromhmb rotate-key, which rotates the age master key; the help text of both commands calls out the distinction in the first line. Reads the new value from stdin (pipe) or--value-from-file, and refuses a--valueargv flag entirely — secrets on the command line are not supported by design. The audit log is created with mode 0600 inside a 0700 parent directory, usesO_APPENDatomic single-writes so concurrent rotations from separate processes interleave cleanly, and passes everyerrorfield through the shared token redaction helper (_redaction.redact_tokens, lifted out of the Bitwarden backend in the same commit) before writing to disk. On success the command printsrotated <credential>; on audit-write failure after a successful rotation it emits a stderr warning and still exits 0, because rolling back a successful rotation to preserve a log line is the wrong trade-off.- Live documentation site (HMB-S022). The mkdocs-material site
built in v0.3.0 is now deployed to
originalrgsec.github.io/himitsubako
on every push to
mainvia a new SHA-pinned workflow at.github/workflows/docs.yml. Docs track the tip ofmain, not the last release tag, so doc-only fixes reach users without waiting for the next version bump. TheDocumentationURL inpyproject.toml[project.urls]has been flipped from the GitHub README anchor to the live Pages URL, so PyPI metadata for v0.4.0 links users to the real site. README carries a newdocsbadge alongside the existingciandPyPIones. - PEP 740 / Sigstore provenance attestations on every PyPI release
(HMB-S023).
attestations: trueis now set on thepypa/gh-action-pypi-publishstep inrelease.yml, and the publish job carries an additionalattestations: writepermission scoped to itself. Every wheel and sdist on pypi.org/project/himitsubako/ from v0.4.0 onward ships with an attached Sigstore attestation bundle bound to the exact GitHub Actions release run that produced it. Downstream users can verify a release with eithergh attestation verifyorpython -m sigstore verify identity; the full guide lives in the docs atsecurity/attestations.md. The threat model adds T-034 (attestation binding misconfiguration), T-038 (downstream non-verification), and mitigations M-027 and M-031.
Changed¶
src/himitsubako/backends/bitwarden.pyimportsredact_tokensfromsrc/himitsubako/_redaction.py. The 40+-char base64 regex that redacts BW_SESSION tokens from Bitwardenbwstderr (HMB-S009 review) has been lifted into a shared helper module so the audit log can reuse it without creating a dependency cycle. Behavior is unchanged; regression-guarded by the existing Bitwarden redaction tests.
Security¶
- T-034, T-035, T-036, T-037, T-038 added to the threat model.
v0.4.0 introduces three new attack surfaces (
hmb rotateaudit log, GitHub Pages deployment, Sigstore attestations) and each is reviewed in a new "v0.4.0 Release Polish Surface Review" section ofthreat-model.md. Highest residual risks are T-038 (downstream non-verification, accepted with documentation mitigation) and T-036 (audit log tampering, accepted as a local log whose threat model is "evidence for me, not against me"). T-035 is mitigated to Low by job-scoped deploy permissions.
Operator actions required before tagging v0.4.0¶
- GitHub Pages enablement. Flip Settings → Pages → Source to
"GitHub Actions" on
github.com/originalrgsec/himitsubako. The workflow cannot enable this itself; without the flip, the firstdocs.ymlrun will fail atactions/deploy-pageswith a missing Pages site error. Completed 2026-04-11 before the sprint-close merge. - TestPyPI dry run — skipped by operator decision. The Sprint 4
plan called for a pre-release tag (
v0.4.0-rc.1) against TestPyPI before the realv0.4.0tag. This step was skipped during sprint close for three reasons: (a) the currentrelease.ymltrigger regex excludes pre-release tags, so exercising it would require a separate TestPyPI workflow and a second Trusted Publisher binding; (b) the attestation change is a single line on an already-tested workflow, not a new release surface; (c) thepypi-releaseenvironment's required-reviewer gate is the real safety net — a failed attestation publish would pause for human approval rather than silently ship bad artifacts. A dedicated TestPyPI dry-run workflow may be added as HMB-S027 in a future sprint if the need arises; for v0.4.0 the risk is accepted.
Test count and coverage¶
- Tests: 190 → 210 (+20 covering
audit.pyandrotate_credential) - Coverage: 85.80% → 86.13%
- All four quality gates green on every commit:
ruff check,ruff format --check,mypy,pytest.
[0.3.1] - 2026-04-11¶
First public PyPI release. The v0.3.0 tag exists in git history but
was never published to PyPI: the release workflow's publish step
failed at docker pull because pypa/gh-action-pypi-publish is a
Docker container action whose registry image is tagged by release
version, not by commit SHA. The action wrapper attempted to pull
ghcr.io/pypa/gh-action-pypi-publish:<commit-sha> and the registry
returned manifest unknown because no such tag exists. Nothing
reached pypi.org during the failed run.
Fixed¶
- Release workflow —
pypa/gh-action-pypi-publishswitched from SHA-pin to version tag (@v1.13.0). Documented as the only Docker-container-action exception to the project's SHA-pinning policy. PyPA does not move version tags, the action is published by the official Python Packaging Authority (also the operator of pypi.org), and v1.13.0 is well past the 7-day quarantine window — the trust delta vs SHA pinning is small in practice. The header comment in.github/workflows/release.ymldocuments the exception in full so future maintainers do not "fix" it back.
No library code changes between v0.3.0 and v0.3.1; the entire diff is the one-line action pin in the release workflow plus this CHANGELOG entry. Everything in the [0.3.0] section below also applies to v0.3.1.
[0.3.0] - 2026-04-11¶
Sprint 3 closes himitsubako's path to PyPI. Seven stories land together:
the CRUD closeout (hmb delete, hmb status), a real integration test
suite that surfaced and fixed a latent SOPS encryption bug that had
broken every hmb set since v0.1.0, a CI pipeline with SHA-pinned
actions and verified sops+age binaries, a full mkdocs-material
documentation site, local-only integration tests for keychain / bw /
direnv, and the release workflow + Trusted Publishers OIDC binding
that publishes to PyPI on every v*.*.* tag.
Added — CLI commands¶
-
HMB-S018 —
hmb deleteCLI command. Removes a secret from the configured backend with a confirmation prompt (--force/--yesto skip,--missing-okfor idempotent cleanup). Routed dispatch names the resolved target backend in the prompt rather than the router wrapper. Exit codes:0success,1not found,2backend error (env backend read-only, keychain denied, etc.). -
HMB-S019 —
hmb statusdiagnostic command. Read-only introspection of the active configuration: config path, default backend, SOPS binary + age recipients from.sops.yaml, theBackendRoutertable in declaration order, and a per-backend ping-style availability check.--jsonemits a single JSON object for scripting. Never reads, writes, or enumerates any credential. Also adds a publicKeychainBackend.check_availability()method.
Added — testing and infrastructure¶
-
HMB-S013 — CI-runnable integration test suite. New
tests/integration/tree with 26 real-binary tests for SOPS and env backends,BackendRouterdispatch, and the fullhmb init → set → get → list → delete → statusCLI flow. Excluded from the defaultuv run pytestvia--ignore=tests/integration; run explicitly withuv run pytest tests/integration/. -
HMB-S014 — GitHub Actions CI pipeline.
.github/workflows/ci.ymlruns ruff check, ruff format check, mypy, unit tests with a--cov-fail-under=80gate, and the S013 integration subset on every push tomainand every PR. Matrix: Python 3.12 and 3.13 onubuntu-latest. Everyuses:reference is SHA-pinned;sopsv3.12.2 andagev1.3.1 are installed from upstream releases with SHA256 verification. Top-levelpermissions: contents: read, concurrency group cancels stale runs, no repo secrets consumed. -
HMB-S020 — local-only integration tests. New test modules for the backends that cannot run in default CI:
test_keychain_real.py(macOS login keychain with UUID-prefixed service and finalizer teardown),test_bitwarden_real.py(gated on an explicitHMB_TEST_BW_SESSIONenv var with per-test folder isolation), andtest_direnv_real.py(realdirenv allow/exec/denyisolation, covering duplicate-marker refusal and shlex-quoted tricky filenames end-to-end). 14 new tests total.
Added — release infrastructure¶
- HMB-S016 — PyPI publish preparation.
pyproject.tomlversion bumped to0.3.0;src/himitsubako/__init__.py__version__matches.project.urlsnow declares Documentation and Changelog URLs. NewCONTRIBUTING.md(development setup, running unit vs integration tests, dependency license discipline, release checklist) andSECURITY.md(supported versions, private vulnerability reporting via GitHub Security Advisories, in-scope and out-of-scope boundaries, regression-guarded defense list). New.github/workflows/release.ymltriggered on finalv*.*.*tags: verify → build → publish jobs. Publish uses Trusted Publishers OIDC (pypa/gh-action-pypi-publish@v1.13.0, SHA-pinned) bound to thepypi-releaseGitHub Actions environment with a required- reviewer approval gate. No long-lived PyPI API tokens. A build-job guard asserts that the git tag,pyproject.tomlversion, andhimitsubako.__version__all agree before any artifact is produced. Local smoke test before this commit: wheel and sdist built viapython -m build,twine check dist/*PASSED, scratch venv install of the wheel reportshmb, version 0.3.0.
Docs¶
- HMB-S015 — mkdocs-material documentation site. New
docs/tree plus top-levelmkdocs.ymlconfiguring thematerialtheme with a light/dark palette toggle and tabbed navigation. Pages: landing, getting-started walkthrough, full CLI reference, configuration andBackendRouterguide, one backend page each for SOPS / env / keychain / bitwarden-cli, integration pages for pydantic-settings and direnv, a user-facing security summary, a "why not ..." section, and a changelog page that rendersCHANGELOG.mdvia the snippets extension.uv run mkdocs build --strictis green in 0.3 s. Deploy target intentionally deferred — the build is the success criterion; GitHub Pages / Read the Docs / Cloudflare Pages selection is a follow-up decision.
Fixed¶
- HMB-S013 (discovered by new integration tests) —
SopsBackend._encryptcould not encrypt against a default-init'd vault. The backend writes to atempfile.mkstemp(suffix=".yaml")tempfile and then callssops --encrypt --in-place <tmpfile>. sops applies.sops.yaml'screation_rulespath_regexagainst the file it's operating on — which is the tempfile name, not.secrets.enc.yaml— so sops aborts witherror loading config: no matching creation rules found. This broke everyhmb set/hmb delete/ rotate path in v0.1.0 through v0.2.0; unit tests did not catch it because subprocess was mocked. The fix passes--filename-override <real_secrets_file>so sops applies the creation_rules against the real target path. Requires sops >= 3.8.0 (the version that introduced--filename-override); the README and backend table now document the minimum. A unit regression test inTestSopsBackendFilenameOverridepins argv ordering so the flag cannot silently drop.
Chore¶
- Codebase-wide
ruff formatpass so the CI format-check stays green. - mypy strict pass over
src/; one pre-existing type narrowing incli/secrets.py::list_secretsannotated asSecretBackend | None. - Register
bitwardenanddirenvpytest markers to support the S020 opt-in local-only suites and the S014 CI filter.
0.2.0 - 2026-04-11¶
Sprint 2 ships the alternate-backend track and the per-credential routing dispatcher that ties them together. v0.2.0 turns himitsubako from "a SOPS wrapper" into "a multi-backend credential abstraction" without breaking any v0.1.x configuration.
Added — backends¶
-
HMB-S007 — first-class environment variable backend.
EnvBackend(prefix: str = "")inhimitsubako.backends.env. Read-only by design (set/deleteraiseBackendError). With a configured prefix,get("DB_PASSWORD")resolvesMYAPP_DB_PASSWORDandlist_keys()returns matching variables with the prefix stripped. The internal_EnvFallbackBackendshim is removed; no-config fallback now returns the realEnvBackend().hmb listagainst an unprefixed env backend emits a stderr warning so users do not mistake inherited shell credentials for app secrets. -
HMB-S008 — macOS Keychain backend.
KeychainBackend(service: str)inhimitsubako.backends.keychain. Wraps thekeyringlibrary (optional[keychain]extra).list_keys()raisesBackendErrorunconditionally because the keyring API does not expose enumeration — the CLI catches this and prints a friendly "this backend does not support listing" message. Insecure-backend deny-list at first call: the resolvedkeyring.get_keyring()is rejected if its MRO matchesNull,PlaintextKeyring,EncryptedKeyring, orfail.Keyring, preventing both direct and subclass-based bypass on misconfigured Linux hosts. -
HMB-S009 — Bitwarden CLI subprocess backend.
BitwardenBackend(folder, bin, unlock_command)inhimitsubako.backends.bitwarden. Invokes thebwsystem binary; nobitwarden-sdkPython dependency (the SDK is non-OSI; see the COR-S037 retrospective). Three modes: - Strict (default):
BW_SESSIONmust be set; the library never prompts. Missing/empty session raises a clearBackendError. - Pinned bin:
bin=constructor arg orHIMITSUBAKO_BW_BINenv var pins an absolute path, mitigating T-005 (PATH hijack ofbw). - Shell-out unlock:
unlock_commandruns a configured command, captures stdout as the master password, pipes it tobw unlock --rawviaBW_PASSWORDenv var (NOT argv) to obtain a session token used in-memory only. Token is never written to disk or logged. Hardened secrecy:BW_SESSIONis never logged or interpolated into errors; the_raise_friendlyhelper redacts any base64 token-like string frombwstderr before re-raising. All subprocess calls use a 30s timeout matching SOPS.
Added — dispatcher¶
- HMB-S012 —
BackendRouterper-credential routing. Newhimitsubako.router.BackendRouterimplementsSecretBackendand dispatches each key to the configured backend. Resolution order: exact match inconfig.credentials→ first matching glob (declaration order,fnmatch.fnmatchcase) →default_backend.list_keys()aggregates across all backends in use; backends that raise onlist_keys(keychain) are caught, logged to stderr as a partial- failure warning, and skipped. Backend instances are cached on first construction. Bothcli/secrets.pyandapi.pywere refactored to return a router rather than a single backend, so all CLI commands and Python API calls transparently support per-credential routing.
Backward compatibility: configs with no credentials: section
behave identically to v0.1.x. All v0.1.x tests pass unchanged.
Added — integrations¶
-
HMB-S010 — direnv helper. New
himitsubako.direnvmodule withgenerate_envrc()andupdate_envrc(). The managed block is delimited by# --- himitsubako start ---and# --- himitsubako end ---markers;update_envrcpreserves any user lines outside the markers and replaces the managed block in place. Idempotent. Refuses to operate on a.envrcwith duplicate markers (would silently corrupt user lines between blocks). Thesecrets_filepath isshlex.quoted before interpolation into the eval line so paths with spaces or shell metacharacters cannot break the eval. Newhmb direnv-exportCLI command regenerates the managed block on demand.hmb inituses the new helper for the initial.envrc;hmb setcallsupdate_envrc()best-effort after a successful sops write. -
HMB-S011 — pydantic-settings source.
HimitsubakoSettingsSourceinhimitsubako.pydanticextendsPydanticBaseSettingsSourceto pull each settings field from a himitsubako backend or router. Use insettings_customise_sourcesto mix backends in a single settings model —db_passwordfrom SOPS,oauth_client_secretfrom Keychain, routed by.himitsubako.yaml. Recommended source order documented in the module:init kwargs > env > himitsubako > dotenv > file_secret > defaults. Optional[pydantic-settings]extra; ImportError converts to a clear BackendError naming the install command.
Config schema additions¶
HimitsubakoConfig.credentials: dict[str, CredentialRoute]— new optional section for per-credential routing.BitwardenConfig.bin: str | NoneandBitwardenConfig.unlock_command: str | None— for HMB-S009.extra=forbidonCredentialRouterejects unknown fields.
Security¶
- T-005 mitigated (HMB-S009):
bwbinary path can be pinned viaHIMITSUBAKO_BW_BINor config to prevent PATH hijack. - T-007 partially mitigated (HMB-S009):
BW_SESSIONis never logged or interpolated into error strings;bwstderr is sanitized to redact base64 token-like substrings before re-raising. The OS-level visibility of env vars to same-user processes remains an accepted limitation of the env-var session model. - T-008 mitigated (HMB-S009): 30-second subprocess timeout on all
bwcalls, matching the SOPS pattern from v0.1.1. - T-020 mitigated (HMB-S008): Keychain access delegates to the OS via the keyring library; first access from a new binary triggers a Touch ID / password prompt on macOS.
- T-022 mitigated via M-014 (HMB-S009): Documentation guidance on
safe
unlock_commandchoices; the library does not log unlock_command output. - T-023 mitigated via M-015 (HMB-S008): Insecure-backend deny-list at first call, with MRO-based subclass detection.
Test state¶
- 80 → 156 passing tests (+76, +95%)
- Coverage 86.27% → 84% (broader surface, same density)
- ruff clean
- Code review (python-reviewer): 2 CRITICAL + 4 HIGH findings, all fixed before tag. Findings included BW_SESSION leak via stderr passthrough (now redacted), BW_PASSWORD env var defense-in-depth cleanup, keychain MRO bypass (now MRO-checked), direnv duplicate markers (now refused), direnv shlex injection (now quoted).
0.1.1 - 2026-04-11¶
Hardening release. Closes the four known limitations flagged at v0.1.0 ship time (threat-model items T-001, T-004, T-010, T-018; ADR open question OQ-4). No new public surface beyond the additions listed below; v0.2.0 alternate backends still land in the next sprint.
Added¶
SopsBackend(secrets_file, sops_bin=None)— new optionalsops_binargument pins thesopsbinary path instead of relying on PATH lookup.HIMITSUBAKO_SOPS_BINenvironment variable — when set and non-empty, takes precedence over both the constructor argument and the config field.sops.binfield in.himitsubako.yaml— optional path to a non-PATHsopsbinary, plumbed through both the CLI (hmb get/set/list/rotate-key) and the Python API. Defaults toNone, preserving v0.1.0 behavior.hmb get KEY --reveal(-r) — boolean flag that authorizes printing the decrypted value to a TTY. When stdout is a pipe or redirect, the flag is optional and the value is printed as before, so$(hmb get KEY)andhmb get KEY | pbcopycontinue to work unchanged.
Changed¶
- All
subprocess.runcalls inSopsBackendnow passtimeout=30s(hardcoded module constant_SOPS_TIMEOUT_SECONDS). Timeouts are caught and re-raised asBackendError("sops", "sops <decrypt|encrypt> timed out after 30s"). SopsBackend._encryptwrites the temp plaintext file with mode0o600from creation (viaos.fchmod), and re-asserts0o600on the destination file after the atomic rename. The destination mode is now independent of the caller's umask.hmb get KEYrunning against a TTY without--revealexits 1 with a stderr message pointing at the flag. This is a deliberate behavior change from v0.1.0; scripts that piped output are unaffected.
Security¶
- T-001 mitigated. A malicious
sopsbinary earlier in PATH can no longer silently shadow the intended one; operators can pin an absolute path via env var or config. - T-004 mitigated. A hung or hostile
sopssubprocess can no longer block himitsubako indefinitely; the 30-second timeout caps the worst case. - T-010 mitigated.
.secrets.enc.yamlis now mode0600regardless of umask, narrowing the local-disclosure window on multi-user systems. - T-018 mitigated.
hmb getno longer prints plaintext to a terminal by default. Shoulder-surfing and terminal scrollback exposure now require an explicit--revealopt-in per invocation. - ADR open question OQ-4 closed: TTY-aware reveal gate selected over config-driven defaults to keep script ergonomics intact while protecting interactive sessions.
0.1.0 - 2026-04-11¶
First rescoped release: the SOPS track ships standalone.
v0.1.0 was originally planned as the full multi-backend release (SOPS, macOS Keychain, Bitwarden CLI, env, direnv, pydantic-settings source, docs site, PyPI publish). After Sprint 1 shipped the SOPS track end-to-end, the version was rescoped down to "SOPS works standalone" so that later multi-backend work lands in v0.2.0 and the public PyPI release lands in v0.3.0. See the project PRD for the new phasing.
This is not yet on PyPI. The public release target is v0.3.0 (HMB-S016).
Added¶
SecretBackendprotocol —@runtime_checkablestructural typing contract withget,set,delete,list_keys, and abackend_nameproperty. Norotatemethod in the protocol; credential-level rotation isset(key, new_value)and age-key rotation is thehmb rotate-keyCLI command.HimitsubakoConfigpydantic model with frozen sub-configs (SopsConfig,KeychainConfig,BitwardenConfig,EnvConfig). v0.1.0 only routes thesopsbackend; otherdefault_backendvalues parse but fail fast at CLI dispatch.find_config()walks up the directory tree to locate.himitsubako.yaml.load_config()parses YAML viayaml.safe_loadand wraps all errors inConfigError.- Error hierarchy:
HimitsubakoError > BackendError > SecretNotFoundError, plusConfigError. - SOPS + age backend:
get,set,delete,list_keysviasopssubprocess; atomic writes via tempfile-plus-rename inlined in the backend. hmb init— creates an age keypair (viaage-keygen) if absent, writes.sops.yaml,.envrc,.secrets.enc.yaml(SOPS-encrypted empty file), and.himitsubako.yaml. Idempotent;--forceto overwrite.hmb get <key>— prints the decrypted value to stdout. Exits 1 with a message to stderr if the key is not found.hmb set <key>— masked prompt by default;--value <v>for scripting.hmb list— prints all key names managed by the configured backend.hmb rotate-key --new-key <path>— updates.sops.yamlrecipients and re-encrypts the project's SOPS file viasops updatekeys.--dry-runprints the plan without executing.- Python API:
himitsubako.get(),set_secret(),list_secrets()with config-driven backend resolution. Fallback chain:.himitsubako.yaml>.sops.yaml> read-only env var fallback. - 16 story files in
docs/stories/covering the v0.1.0 → v0.3.0 roadmap (Sprint 1 through Sprint 3).
Security¶
SecretNotFoundErrorexcludes credential key names from its string representation; the missing key is still accessible programmatically viaerr.key.- Broad
exceptin config loading narrowed toValueError | TypeError. types-PyYAMLadded for static analysis confidence on YAML parsing paths.- SOPS backend tests assert that credential values do not appear in captured output.
hmb setmasks prompted input viaclick.prompt(hide_input=True).yaml.safe_load()used exclusively (noyaml.loadoryaml.unsafe_load).
Known Limitations (v0.1.1 hardening targets)¶
hmb getprints the full plaintext value to stdout with no--revealgate. Redacted-by-default output is the top v0.1.1 priority..secrets.enc.yamlis written with the default umask (usually 0644) rather than an explicit 0600 chmod.- The SOPS backend resolves
sopsvia PATH only and does not set a subprocess timeout.
Deferred¶
| Feature | Deferred to | Story |
|---|---|---|
| First-class env backend (CLI-routable) | v0.2.0 | HMB-S007 |
| macOS Keychain backend | v0.2.0 | HMB-S008 |
| Bitwarden CLI backend | v0.2.0 | HMB-S009 |
| direnv integration helper | v0.2.0 | HMB-S010 |
| pydantic-settings source | v0.2.0 | HMB-S011 |
| Per-credential backend routing | v0.3.0 | HMB-S012 |
| Real-binary integration tests | v0.3.0 | HMB-S013 |
| CI pipeline (GitHub Actions) | v0.3.0 | HMB-S014 |
| mkdocs documentation site | v0.3.0 | HMB-S015 |
| PyPI publication | v0.3.0 | HMB-S016 |