fix: pin directory trust to programa.json contents - #204
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.jsonin 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
Sources/ProgramaDirectoryTrust.swift— everything else is fallout.TrustState(trusted/changed/untrusted) andtrustState(configPath:globalConfigPath:).isTrustednow delegates to it, so its signature and every existing caller are unchanged.executableDigest(forConfigAt:)reusesJSONCParser.preprocessrather than reimplementing comment stripping.["/path"]to{"version": 2, "directories": {"/path": "<sha256>"}}. The old array still decodes, so existing trust survives the upgrade.Sources/ProgramaConfigExecutor.swift—confirmIfUntrustedresolves the three-state value;changedforces confirmation even for entries withoutconfirm: true, andshowConfirmDialogprepends the changed-warning line.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.trustDirectoryand.messageWithCommandwere 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 theinit(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.changed; deleted config reportsuntrustedreplaceAll(with:)entries adopt then enforce