Phase log 32: 2026-09-14 - Planning Phase 24 and 24.1–24.4
A planning session. No source file was changed. Only CLAUDE.md (gitignored)
and this file were written.
Goal
The user sent twenty items - defects in shipped features, a red CI job, and five pieces of new surface - and asked for them to be planned as Phase 24, with the existing 24 onward pushed back by one, with questions asked to settle the concepts before writing the plan.
Current Progress
- Renumbered. Old 24 (Storage) → 25, … old 38 (Pentest) → 39. A third
renumbering map (2026-09-14) is in
CLAUDE.md; "Why this order" and the dependency edges were rewritten against the new numbers. The next phase to allocate is 40. - Phase 24 planned as defects + lint + CI hygiene + docs site, with four
sub-phases: 24.1 composite secrets, 24.2 secrets-grid legibility,
24.3 calendar feeds, 24.4 unique-ID registry. Full design in
CLAUDE.mdunder Phase 24. - Three stale lines fixed in passing: version (said 0.20.0, is 0.23.0), Phase 23's section status (said steps 0–3, is all done), the next handoff number.
- Review-01 §8 (the lint backlog) moved from Phase 26 into Phase 24, because it is what turned CI red.
The findings that mattered
Found while grounding the plan, ranked by consequence. None fixed - this was a planning session.
- Every 2FA code is blank on a remote vault - verified in source, confirmed
by the user's symptom.
entry_totp_coderefuses unless the local SQLCipher key is held; on a remote session it is not, andliveCodeForswallows the error. The user suspected a migration; it is not one. The whole Authenticator feature looks broken to anyone usingenvv-server. - Probably every app export writes nothing while toasting success.
downloadTextclicks a blob<a download>and toasts unconditionally; 15 call sites; no dialog/fs plugin and no download handler insrc-tauri. Reported only for ICS. Unverified - reproduce with two exports first. - CI's lint exit code 2 is stale suppressions, not the 501 warnings. The
diagnosis the user pasted blamed the warnings and recommended
--suppress-all, whichCLAUDE.mdforbids.npx eslint --prune-suppressionsalone turns CI green. The full backlog is 501 warnings plus 236 suppressed errors. - Inject can never work. It requires the Add/Edit overlay open, and the Tools panel it lives in sits behind that overlay.
- Seed-only entries already save (
primaryIsOptional), so "standalone 2FA" meant a flow, not a validation fix. - There is no emoji-insertion code; the report is about the OS emoji picker not reaching the WebKitGTK webview.
Decisions made (with the user)
| Question | Answer |
|---|---|
| Structure | 24 + sub-phases 24.1–24.4, the 22.2 pattern |
| Nested-secret name | composite - template + parts |
| Placeholders | named {calendar_id}, never positional |
| Pool grouping | one card that expands; copy uses the pool cursor |
| UID registry storage | keyed hash only |
| UID registry verbs | check at mint, lookup, register external; rate-limit check and lookup; prune before a date; no tombstones |
| ICS as a link | envv-server + LAN, per-user token URL |
| Shareable docs | committed docs site on Pages (book/) |
| Standalone 2FA | "Add 2FA" flow on the Authenticator panel, no new SecretType |
* on env value | the required marker ignores primaryIsOptional |
| Inject | generator popover inside the form |
| Caps Lock | ask the OS, typed-letter derivation as fallback |
| Type toggles | multi-toggle chip bar |
Decisions of my own, recorded in CLAUDE.md with rationale: composite parts
reuse extra_vars; the composite template is printed, not masked; the registry
lives in its own registry.db keyed from vault_meta; a locked server answers a
feed with 503, never an empty calendar; removing the sidebar section is written as
a reversal of Phases 22 and 22.2.
What Worked / What Didn't Work
- Worked: reading the code before asking. Three of the four question rounds were shaped by something found in source (the local-key gate, the backdrop over Tools, the seed-only predicate), and the user's answer to "local or remote?" confirmed the A1 mechanism outright.
- Worked: running
npm run lintrather than trusting the pasted diagnosis. - Didn't: I could not reproduce anything in a real window (no vault password, no display). A3, A4, A6, A7, A9 and A10 are hypotheses and are labelled so.
Next Steps
- Phase 24, in the order written in
CLAUDE.md: prune suppressions first. - Before A3, reproduce: Timeline → export ICS, and a
.envexport, incargo tauri dev. - Before A7 and A6, a screenshot of the secrets sidebar and of a card holding a seed plus named variables.
At the start of D, the user picks from the five handoff-integration options.Decided later the same session: decision records (see the continuation below).- Only the human can do: enable Dependabot alerts/security updates, private vulnerability reporting and code scanning in repository settings.
Notes for whoever picks this up
CLAUDE.mdandthe session records/are gitignored; there is nothing to commit from this session.- The open questions for 24.1, 24.3 and 24.4 were answered in the continuation
below; three of the answers are recommendations the user asked to have
explained and has not yet confirmed, and are marked so in
CLAUDE.md. - A1's fix removes a gate. Audit the sibling commands with the same
"Vault is locked"check before deciding which ones to change; some really do need the local database.
Continued, same day - research pass: bundles, sessions, types, registry numbers
Decisions added (with the user)
| Question | Answer |
|---|---|
| 2FA on remote vaults | show codes; no warning about seed and password in one vault |
| ICS builder | (recommended) one Rust builder over IPC; TS builder deleted; fixture kept |
| Composite encoding | (recommended) store raw, encode by URL zone, warn on pre-encoded |
basic copy of a composite | (recommended) rendered value only; parts from extended up |
| Registry uniqueness | global, across every namespace |
| Handoffs in docs | decision records (ADRs) with a citation-checking test |
| Merged cards | bundle; members stay real entries; one bundle per entry |
| Cookies | generalised to a web session: cookies + storage tokens + header recipe + bindings + presets |
| New types | all three sets - 16 types |
| Key metadata | orthogonal axes, prefix auto-fill |
| CXF | import + export |
| Placement | 24.1 = composite + bundle; new 24.5 = registry of types, new types, sessions, taxonomy, CXF |
Findings from this pass
-
A11 - older builds downgrade unknown types on Save.
modals.ts:670sets the type select to a value it has no option for (reads back""), andmodals.ts:252falls back to'api_key'. Must ship in a release before 24.1. -
filter_vault_for_userrebuilds the vault from three known keys, so any new top-level array (bundles) disappears for sub-users; the write merge preserves it. Recorded as B6/B7. -
Registry benchmark (SQLCipher, rusqlite 0.31 - the pinned version - WAL, 16-byte HMAC key,
WITHOUT ROWID, date index; this 4-core desktop):Rows Bulk insert/s (1000/tx) Single-commit insert/s Lookup/s Size Prune 10% 1M (plain SQLite, for contrast) 70,986 33,660 152,859 59.7 MB 0.59 s 1M 24,780 12,095 56,413 61.1 MB 3.28 s 10M 17,505 10,663 37,475 610 MB 45.7 s in one transaction HMAC-SHA256 ≈ 756k/s - never the bottleneck. 61 bytes per row, flat from 1M to 10M. The 45.7 s single-statement prune holds the writer lock the whole time, which is why prune is now chunked in the plan.
Sources
- FIDO CXF: FIDO Credential Exchange Specifications, go-cxf field reference, credential-exchange-format crate, CXF v1.0 Proposed Standard issue
- DBSC: Chrome for Developers, Help Net Security, 2026-04-10
- X cookies: twscrape, yt-dlp #16176
- Instagram: instagram-private-api #877, instagram_private_api client
- YouTube SAPISIDHASH: gist, yt-dlp cookie auth refactor
- LinkedIn: Voyager API via browser cookies
- Spotify: den.dev, SpotifyScraper auth
- NextAuth chunking: next-auth cookie.ts, discussion #2628
- cf_clearance: crawlex, scrapeless
- Chrome app-bound encryption: yt-dlp #10927, The Register
- Token prefixes: GitHub token formats, OpenAI key formats, Slack token types, Slack xoxc/xoxd
- Rate-limit prior art: Vault rate-limit quotas, Infisical API limits
Not verified this session and marked so in CLAUDE.md: Reddit/TikTok/Discord session
cookies, Stripe prefixes, which managers ship a CXF export button.
Registry benchmark, second run
Pinned to 2 cores (taskset -c 0,1), PRAGMA cache_size=-32000, chunked prune
(10,000 rows per transaction), 10M rows: bulk 24,429/s, one commit per ID
11,971/s, lookups 55,416/s, 610 MB, prune of 1M rows 36.2 s total with the worst
single chunk 0.38 s. The no-index (month-partition) variant was stopped before
finishing at the user's request; single table + date index was chosen without it.
Killing it with pkill -f also killed the invoking shell (exit 144) - the
Pitfalls entry about pkill -f matching its own command line, hit again.
Continued again - bundles as scopes (the Discord bot config)
The user asked for composites that reference bundle siblings and for a bundle to
be able to hold a whole config file, using an old Discord bot config (all values
revoked) as the example. It is a Python module, not a dotenv: f-strings, a hex
colour, snowflake ids, markdown links, and api_key assigned twice.
| Question | Answer |
|---|---|
| Reference syntax inside a bundle | {slot.field} for a member, {name} for a local |
| Small non-secret values | bundle-local variables |
| Derived values on export | rendered by default, per format (f-strings in Python, template literals in JS) |
| Value kinds | all four groups: core + colour, templates, large ids + bitfields, dev formats |
Design change made because of it: a bundle is now an entry of
secretType: 'bundle' whose extra_vars are its local variables, replacing the
top-level VaultData.bundles array written earlier the same day. It gets RBAC,
masking, history and env naming for free, older builds can read it, and Phase 25
has no bundle table to migrate. Recorded in CLAUDE.md as a same-day revision.
Findings from the example, each now an edge case with a rule: Discord snowflakes
exceed 2^53 and silently round as JS numbers (B15, large_id); the duplicate
api_key binds positionally in Python, so the importer suffixes and rewrites
later references rather than letting the last one win; a composite over siblings
must carry taint so a URL containing api_key stays masked while the invite
link does not (resolution rules, B8).