{"record":{"id":"bb2a715ea2b31afd","repo":"jdx/mise","slug":"cannot-publish-encrypted-path-has-an-unencrypted-or-invalid","errorCode":null,"errorMessage":"cannot publish: encrypted path {} has an unencrypted or invalid version in commit {commit}; encrypting the latest version does not erase earlier plaintext. Remove that history before pushing, or use `mise dot sync --allow-plaintext-history` to publish it anyway","messagePattern":"cannot publish: encrypted path (.+?) has an unencrypted or invalid version in commit (.+?); encrypting the latest version does not erase earlier plaintext\\. Remove that history before pushing, or use `mise dot sync --allow-plaintext-history` to publish it anyway","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/system/history/sync/files.rs","lineNumber":131,"sourceCode":"            if !protected.iter().any(|path| {\n                entry.path == *path\n                    || entry\n                        .path\n                        .strip_prefix(path)\n                        .is_some_and(|rest| rest.starts_with('/'))\n            }) || !checked.insert((entry.path.clone(), entry.mode.clone(), entry.oid.clone()))\n            {\n                continue;\n            }\n            let object = (entry.mode, entry.oid);\n            let valid =\n                envelope(repo, &object, agecrypt::MAX_ENCRYPTED_BYTES)?.is_some_and(|outer| {\n                    outer.path == entry.path\n                        && matches!(outer.mode.as_str(), \"100644\" | \"100755\" | \"120000\")\n                        && outer.ciphertext.0.starts_with(b\"age-encryption.org/v1\\n\")\n                });\n            if !valid {\n                bail!(\n                    \"cannot publish: encrypted path {} has an unencrypted or invalid version in commit {commit}; encrypting the latest version does not erase earlier plaintext. Remove that history before pushing, or use `mise dot sync --allow-plaintext-history` to publish it anyway\",\n                    entry.path\n                );\n            }\n        }\n    }\n    if let Ok(mut cache) = AUDITS.lock() {\n        // One watcher normally uses one repository. Bound temporary onboarding\n        // probes without adding another durable history or state file.\n        if cache.len() >= 64 && !cache.contains_key(repo.dir()) {\n            cache.pop_first();\n        }\n        cache.insert(\n            repo.dir().to_path_buf(),\n            VerifiedAncestry {\n                head: head.into(),\n                protected,\n            },","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/history/sync/files.rs#L113-L149","documentation":"When publishing (pushing) history, audit_history_cached walks every commit reachable from the remote head and checks that each version of a path marked as encrypted is a genuine, well-formed age-encrypted envelope (correct path, regular-file/symlink mode, and the age-encryption.org/v1 header). If any historical commit contains a plaintext or corrupt version of an encrypted path, publishing is blocked — because pushing would leak plaintext that 'encrypting the latest version' cannot erase. The user must rewrite/remove that history or explicitly opt in with --allow-plaintext-history.","triggerScenarios":"`mise dot sync` push with audit_history_cached detecting, for some commit {commit}, an entry whose path is in the encrypted set but whose blob is not a valid envelope (unencrypted plaintext, wrong mode, or ciphertext missing the age header). Typically a file was committed before encryption was enabled for that path.","commonSituations":"You enabled encryption for a secrets file after it had already been committed in plaintext to dot history; a corrupted/interrupted encryption write left a bad blob in an old commit; a history import brought in pre-encryption versions.","solutions":["If the plaintext history is safe to keep and you accept the risk, re-run with `mise dot sync --allow-plaintext-history` to publish anyway.","Otherwise remove/rewrite the offending history (prune or rebase the history store so the plaintext commit is gone), then push again.","Identify the leaking commit (named in the message) and re-encrypt or drop that version of the path.","Going forward, enable encryption for a path before its first commit so plaintext versions never enter history."],"exampleFix":"// before\n$ mise dot sync   # push\nerror: cannot publish: encrypted path .envrc has an unencrypted or invalid version in commit abc123...\n// after (explicit opt-in)\n$ mise dot sync --allow-plaintext-history\n// or: rewrite history to drop commit abc123, then\n$ mise dot sync","handlingStrategy":"validation","validationCode":"# before pushing, confirm no plaintext versions of encrypted paths exist in history\nmise dot log -- <encrypted-path>   # or inspect history commits; require all versions to be age envelopes\n# explicit acknowledgement if intentional:\nmise dot sync --allow-plaintext-history","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Enable encryption for a path before its first commit to history.","Periodically audit history for plaintext versions of secret paths before pushing.","Never silence with --allow-plaintext-history unless you accept leaking old plaintext.","Treat 'encrypt after the fact' as insufficient — rewrite history to drop plaintext commits."],"tags":["security","encryption","secrets","git-history"],"backgroundTag":"checksum-mismatch","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}