Skip to content

fix: pin directory trust to programa.json contents - #204

Merged
arzafran merged 1 commit into
mainfrom
fix/188-pin-trust-to-config-contents
Jul 30, 2026
Merged

fix: pin directory trust to programa.json contents#204
arzafran merged 1 commit into
mainfrom
fix/188-pin-trust-to-config-contents

Conversation

@arzafran

Copy link
Copy Markdown
Member

Closes #188

What this does

Ticking "always trust commands from this folder" used to be a blank cheque. It recorded the folder, never what you actually approved, so if someone changed programa.json in that repo and you pulled, the new commands just ran. You'd effectively approved a file you never saw.

Trust is now pinned to the config's contents. Change a command and you get asked again, with the dialog saying the file changed rather than appearing out of nowhere. Everything else stays silent like before.

Comments and formatting deliberately don't count. The digest is taken after JSONC comments and trailing commas are stripped and the JSON is re-serialized with sorted keys, so reformatting the file or editing a comment won't nag you — only a change to something that actually runs will.

Summary

  1. Start with Sources/ProgramaDirectoryTrust.swift — everything else is fallout.
    • New TrustState (trusted / changed / untrusted) and trustState(configPath:globalConfigPath:). isTrusted now delegates to it, so its signature and every existing caller are unchanged.
    • executableDigest(forConfigAt:) reuses JSONCParser.preprocess rather than reimplementing comment stripping.
    • On-disk schema goes from ["/path"] to {"version": 2, "directories": {"/path": "<sha256>"}}. The old array still decodes, so existing trust survives the upgrade.
  2. Sources/ProgramaConfigExecutor.swiftconfirmIfUntrusted resolves the three-state value; changed forces confirmation even for entries without confirm: true, and showConfirmDialog prepends the changed-warning line.
  3. Resources/Localizable.xcstrings — the new string, plus a backfill (see below).

Decisions worth flagging

Legacy entries adopt silently. An entry with no digest records the current one on first run without prompting. Zero upgrade friction, and the tradeoff is deliberate: if a config was already tampered with before this shipped, that state becomes the baseline. The alternative was one prompt per trusted repo at upgrade.

One-click re-trust, no diff view. Showing what changed would mean storing the whole trusted config rather than a digest. Worth doing separately if it's wanted.

Fails closed. A trusted config that can't be read or parsed resolves to untrusted, never "still trusted".

Drive-by fix

dialog.cmuxConfig.confirmCommand.trustDirectory and .messageWithCommand were referenced by this dialog but absent from the string catalog entirely — every sibling key in the same family was registered. Both backfilled with en + ja, since this PR was already in that dialog.

Note on the two-commit test policy

CLAUDE.md asks for failing-test-then-fix so CI proves the test catches the bug. It doesn't apply cleanly here: the tests exercise trustState, TrustState, and the init(storePath:) seam, all introduced by the fix, so a test-only commit wouldn't compile rather than fail. Splitting it would have meant committing a deliberately stubbed API purely for theatre.

Test plan

programaTests/ProgramaDirectoryTrustTests.swift — 11 tests, each against a throwaway store in a temp dir, never the real one.

  • Legacy flat-array store decodes and adopts trust on first query
  • Once adopted, a changed command is caught
  • Comment-only edit stays trusted; whitespace + key-reorder edit stays trusted
  • Changed command reports changed; deleted config reports untrusted
  • replaceAll(with:) entries adopt then enforce
  • CI green

Trusting a folder used to be permanent and untied to what the config
actually said when it was approved, so a later commit to programa.json
ran with no prompt at all.

Each trusted entry now records a SHA-256 of the config's executable
content: JSONC comments and trailing commas stripped, then re-serialized
with sorted keys. Editing a comment or reformatting the file does not
re-prompt; changing a command does.

- store schema moves from a flat [String] to a versioned object, with
  the old array still decoded so existing trust survives the upgrade
- entries with no digest (legacy, or arriving via replaceAll) adopt the
  current digest silently on first query, then enforce from then on
- a trusted config that can no longer be read fails closed to untrusted
- changed content always confirms, even for entries without confirm: true

Also backfills dialog.cmuxConfig.confirmCommand.trustDirectory and
.messageWithCommand, which were used by this dialog but missing from the
string catalog entirely.
@arzafran
arzafran merged commit 9cd9326 into main Jul 30, 2026
10 checks passed
@arzafran
arzafran deleted the fix/188-pin-trust-to-config-contents branch July 30, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trusting a repo's programa.json is permanent, so a later commit to it runs without asking

1 participant