@bman654/clodex 2.12.0 → 2.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -198,9 +198,9 @@ Patch the installed Claude Code binary so clodex favorites and aliases are first
198
198
  | `--disable-local-patches` | Disable local patches and rebuild from pristine bytes without them |
199
199
  | `--help` | Help |
200
200
 
201
- On Windows, when Claude Code was installed with `npm install -g`, the `claude` on your PATH is a small launcher script (`claude.cmd`, `claude.ps1`, and an extensionless one) rather than the program itself; `clodex patch` follows it to the program and patches that. (On macOS and Linux npm makes a symlink instead, which always worked.) If the launcher cannot be followed — its program moved or removed, or it names two different programs — `clodex patch` stops without touching anything and tells you to set `CLODEX_CLAUDE_PATH` to the program directly.
201
+ On Windows, when Claude Code was installed with `npm install -g`, the `claude` on your PATH is a small launcher script (`claude.cmd`, `claude.ps1`, and an extensionless one) rather than the program itself; `clodex patch` follows it to the program and patches that. (On macOS and Linux npm makes a symlink instead, which always worked.) If the launcher cannot be followed — its program moved or removed, or it names two different programs — `clodex patch` stops without touching anything and tells you to set `TWEAKCC_CC_INSTALLATION_PATH` to the program directly.
202
202
 
203
- The patch map is built from your favorites and aliases; context windows come from provider metadata. A pristine per-version backup is kept, and a manifest (`~/.clodex/patch-state.json`) makes re-runs no-ops until your config or Claude Code version changes — then the binary is restored first and re-patched fresh. `clodex claude` checks patch freshness at launch and offers to re-patch (a non-blocking notice when not interactive). Re-run `clodex patch` after every `claude` update.
203
+ The patch map is built from your favorites and aliases; context windows come from provider metadata. A pristine per-version backup is kept — recorded against the install it was made for, so a machine with two installs of one Claude Code version restores each from its own bytes — and a manifest (`~/.clodex/patch-state.json`) makes re-runs no-ops until your config or Claude Code version changes — then the binary is restored first and re-patched fresh. `clodex claude` checks patch freshness at launch and offers to re-patch (a non-blocking notice when not interactive). Re-run `clodex patch` after every `claude` update.
204
204
 
205
205
  #### Local patches (trusted code)
206
206
 
@@ -337,7 +337,10 @@ clodex --version # version
337
337
  `CLODEX_CREDENTIAL_HELPER` to an absolute executable path to use an external
338
338
  secure store instead; see [credential helpers](docs/credential-helpers.md).
339
339
  - Proxied routes forward configured provider headers for API-key and OAuth authentication. Anonymous routes preserve non-credential headers while removing authorization, API-key, cookie, token, secret, and credential-bearing header names before dispatch.
340
- - `CLODEX_CLAUDE_PATH` overrides Claude Code binary discovery.
340
+ - `CLODEX_CLAUDE_PATH` overrides which Claude Code gets **launched**. It does not choose what
341
+ `clodex patch` writes to — set `TWEAKCC_CC_INSTALLATION_PATH` for that, and `clodex patch`
342
+ says so when `CLODEX_CLAUDE_PATH` points somewhere else. The two are separate because a
343
+ `CLODEX_CLAUDE_PATH` aimed at a wrapper script is right for launching and wrong for patching.
341
344
  - **Codex service tier:** `CLODEX_SERVICE_TIER` accepts `fast` (normalized to
342
345
  `priority`), `priority`, `flex`, `auto`, or `default`. Clodex requests the
343
346
  resolved value only after selecting a ChatGPT/Codex OAuth route; OpenAI
package/dist/cli.js CHANGED
@@ -382,7 +382,7 @@ import { join } from "path";
382
382
  // package.json
383
383
  var package_default = {
384
384
  name: "@bman654/clodex",
385
- version: "2.12.0",
385
+ version: "2.12.1",
386
386
  publishConfig: {
387
387
  access: "public"
388
388
  },
@@ -3884,16 +3884,17 @@ function applyClodexPatches(source, config) {
3884
3884
  // src/patcher.ts
3885
3885
  import { createHash as createHash9 } from "crypto";
3886
3886
  import {
3887
+ chmodSync as chmodSync4,
3887
3888
  copyFileSync,
3888
3889
  existsSync as existsSync5,
3889
3890
  mkdtempSync,
3890
3891
  mkdirSync as mkdirSync5,
3891
3892
  readFileSync as readFileSync10,
3892
- renameSync as renameSync2,
3893
- rmSync,
3893
+ renameSync as renameSync3,
3894
+ rmSync as rmSync2,
3894
3895
  statSync as statSync8,
3895
3896
  unlinkSync as unlinkSync3,
3896
- writeFileSync as writeFileSync5,
3897
+ writeFileSync as writeFileSync6,
3897
3898
  openSync as openSync7,
3898
3899
  closeSync as closeSync7,
3899
3900
  realpathSync as realpathSync2
@@ -7772,7 +7773,7 @@ function writeAll(fd, bytes, position, path) {
7772
7773
 
7773
7774
  // src/patch-backup.ts
7774
7775
  import { createHash as createHash5 } from "crypto";
7775
- import { existsSync as existsSync4, readFileSync as readFileSync9, readdirSync, statSync as statSync7 } from "fs";
7776
+ import { existsSync as existsSync4, readFileSync as readFileSync9, readdirSync, renameSync as renameSync2, rmSync, statSync as statSync7, writeFileSync as writeFileSync5 } from "fs";
7776
7777
  import { homedir as homedir2 } from "os";
7777
7778
  import { join as join7 } from "path";
7778
7779
  var BACKUP_SHA_PREFIX_LENGTH = 16;
@@ -7790,9 +7791,60 @@ function backupVersionTag(version) {
7790
7791
  function contentAddressedBackupPath(version, sha256, dir = backupDir()) {
7791
7792
  return join7(dir, `claude-${backupVersionTag(version)}-${sha256.slice(0, BACKUP_SHA_PREFIX_LENGTH)}.orig`);
7792
7793
  }
7794
+ function legacyBackupPath(version, dir = backupDir()) {
7795
+ return join7(dir, `claude-${backupVersionTag(version)}.orig`);
7796
+ }
7793
7797
  function tweakccMirrorBackupPath(dir = backupDir()) {
7794
7798
  return join7(dir, "native-binary.backup");
7795
7799
  }
7800
+ function installProvenancePath(backupPath, binaryPath) {
7801
+ const tag = createHash5("sha256").update(binaryPath).digest("hex").slice(0, BACKUP_SHA_PREFIX_LENGTH);
7802
+ return `${backupPath}.for-${tag}.json`;
7803
+ }
7804
+ function installProvenancePattern(name) {
7805
+ return new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.for-[0-9a-f]{${BACKUP_SHA_PREFIX_LENGTH}}\\.json$`);
7806
+ }
7807
+ function readInstallProvenance(path) {
7808
+ let raw;
7809
+ try {
7810
+ raw = readFileSync9(path, "utf8");
7811
+ } catch {
7812
+ return null;
7813
+ }
7814
+ let parsed;
7815
+ try {
7816
+ parsed = JSON.parse(raw);
7817
+ } catch {
7818
+ return "damaged";
7819
+ }
7820
+ if (typeof parsed !== "object" || parsed === null) return "damaged";
7821
+ const { install, assumed } = parsed;
7822
+ if (typeof install !== "string" || install === "") return "damaged";
7823
+ if (typeof assumed !== "boolean") return "damaged";
7824
+ return { install, assumed };
7825
+ }
7826
+ function recordBackupProvenance(backupPath, binaryPath, opts) {
7827
+ const target = installProvenancePath(backupPath, binaryPath);
7828
+ const existing = readInstallProvenance(target);
7829
+ if (existing && existing !== "damaged" && existing.install === binaryPath) {
7830
+ if (existing.assumed === opts.assumed) return existing.assumed ? "assumed" : "established";
7831
+ if (!existing.assumed) return "established";
7832
+ }
7833
+ const record = { install: binaryPath, assumed: opts.assumed };
7834
+ const temp = `${target}.tmp-${process.pid}-${Date.now().toString(36)}`;
7835
+ try {
7836
+ writeFileSync5(temp, `${JSON.stringify(record, null, 2)}
7837
+ `);
7838
+ renameSync2(temp, target);
7839
+ return opts.assumed ? "assumed" : "established";
7840
+ } catch (err) {
7841
+ try {
7842
+ rmSync(temp, { force: true });
7843
+ } catch {
7844
+ }
7845
+ throw err;
7846
+ }
7847
+ }
7796
7848
  function scanPristineBackups(version, dir = backupDir()) {
7797
7849
  const tag = backupVersionTag(version);
7798
7850
  const pattern = new RegExp(`^claude-${tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:-([0-9a-f]{${BACKUP_SHA_PREFIX_LENGTH}}))?\\.orig$`);
@@ -7802,6 +7854,27 @@ function scanPristineBackups(version, dir = backupDir()) {
7802
7854
  } catch {
7803
7855
  return { valid: [], corrupt: [] };
7804
7856
  }
7857
+ const provenanceOf = (name) => {
7858
+ const recordPattern = installProvenancePattern(name);
7859
+ const installs = [];
7860
+ const assumedInstalls = [];
7861
+ const damagedProvenance = [];
7862
+ for (const entry of entries) {
7863
+ if (!recordPattern.test(entry)) continue;
7864
+ const path = join7(dir, entry);
7865
+ const record = readInstallProvenance(path);
7866
+ if (record === null || record === "damaged" || installProvenancePath(join7(dir, name), record.install) !== path) {
7867
+ damagedProvenance.push(path);
7868
+ continue;
7869
+ }
7870
+ (record.assumed ? assumedInstalls : installs).push(record.install);
7871
+ }
7872
+ return {
7873
+ installs: installs.sort(),
7874
+ assumedInstalls: assumedInstalls.sort(),
7875
+ damagedProvenance: damagedProvenance.sort()
7876
+ };
7877
+ };
7805
7878
  const valid = [];
7806
7879
  const corrupt = [];
7807
7880
  for (const entry of entries.sort()) {
@@ -7822,9 +7895,9 @@ function scanPristineBackups(version, dir = backupDir()) {
7822
7895
  corrupt.push(path);
7823
7896
  continue;
7824
7897
  }
7825
- valid.push({ path, kind: "content-addressed", sha256 });
7898
+ valid.push({ path, kind: "content-addressed", sha256, ...provenanceOf(entry) });
7826
7899
  } else {
7827
- valid.push({ path, kind: "legacy", sha256 });
7900
+ valid.push({ path, kind: "legacy", sha256, ...provenanceOf(entry) });
7828
7901
  }
7829
7902
  }
7830
7903
  return { valid, corrupt };
@@ -7846,7 +7919,7 @@ function looksLikeLegacyClodexPatch(source) {
7846
7919
  }
7847
7920
  function noBackupMessage(facts) {
7848
7921
  const corrupt = facts.corruptBackups?.length ? ` (${facts.corruptBackups.length} backup file(s) for this version failed integrity checks and were ignored)` : "";
7849
- return `claude ${facts.version} is already patched and no trustworthy pristine backup for that version exists in ${backupDir()}${corrupt}. Reinstall Claude Code to get a pristine binary, then run \`clodex patch\`.`;
7922
+ return `clodex holds no pristine backup of claude ${facts.version} it can attribute to ${facts.binaryPath}: none was found in ${backupDir()}${corrupt}. If that binary is patched, reinstall Claude Code to get a pristine one and run \`clodex patch\` again. If it was never patched, there is nothing to restore.`;
7850
7923
  }
7851
7924
  function identifiesABackup(manifest) {
7852
7925
  return manifest.pristineSha256 !== void 0 || manifest.backupPath !== void 0;
@@ -7860,8 +7933,26 @@ function recordedBackupGoneMessage(facts, manifest) {
7860
7933
  function otherInstallMessage(facts, otherBinaryPath) {
7861
7934
  return `The patch manifest records a different Claude Code install (${otherBinaryPath}), so nothing establishes that a pristine backup tagged claude ${facts.version} in ${backupDir()} belongs to ${facts.binaryPath}. Two installs of one Claude Code version are different files, so restoring by version tag alone would overwrite this install with another one's bytes. Set TWEAKCC_CC_INSTALLATION_PATH=${otherBinaryPath} to restore that install instead, or reinstall Claude Code to make this one pristine.`;
7862
7935
  }
7936
+ function otherInstallProvenanceMessage(facts, recorded) {
7937
+ const owners = [...new Set(recorded.flatMap((backup) => backup.installs))];
7938
+ return `The pristine backup(s) of claude ${facts.version} in ${backupDir()} are recorded as the pristine content of ${owners.join(", ")}, not of ${facts.binaryPath}. Two installs of one Claude Code version are different files, so restoring one of those would overwrite this install with bytes that were never its own. Set TWEAKCC_CC_INSTALLATION_PATH=${owners[0]} to restore that install instead, or reinstall Claude Code to make this one pristine.`;
7939
+ }
7940
+ function damagedProvenanceMessage(facts, damaged) {
7941
+ return `A pristine backup of claude ${facts.version} in ${backupDir()} carries a provenance record that clodex cannot read (${damaged.join(", ")}). Something recorded which install those bytes belong to and that record is damaged, so clodex will not fall back to matching on the claude version in the file name \u2014 on a machine with more than one install that would hand ${facts.binaryPath} bytes that were never its own. Delete the unreadable record to accept that fallback, or reinstall Claude Code to make this install pristine.`;
7942
+ }
7943
+ function contradictoryProvenanceMessage(facts, mine) {
7944
+ return `More than one pristine backup of claude ${facts.version} is recorded as the pristine content of ${facts.binaryPath} (${mine.map((backup) => backup.path).join(", ")}), and they do not hold the same bytes, so clodex cannot tell which one that install holds now \u2014 being snapshotted twice, at two different sets of unpatched bytes, reaches this state without either record being wrong. Delete whichever is stale to decide it: ${mine.map((backup) => installProvenancePath(backup.path, facts.binaryPath)).join(", ")}. Or reinstall the Claude Code at ${facts.binaryPath} and delete both.`;
7945
+ }
7946
+ function manifestContradictsRecordMessage(facts, manifestChoice, recorded) {
7947
+ return `The patch manifest names ${manifestChoice} as the pristine content of ${facts.binaryPath}, but ${recorded.map((backup) => backup.path).join(", ")} is recorded as that install's pristine content and holds different bytes. Nothing establishes which of them the install at that path is now \u2014 an executable replaced in place with a different build of the same claude version reaches exactly this state \u2014 so clodex will not pick one. Delete whichever of these records is stale to decide it: ${recorded.map((backup) => installProvenancePath(backup.path, facts.binaryPath)).join(", ")}. Or reinstall the Claude Code at ${facts.binaryPath} and delete both: a pristine install needs no backup, and \`clodex patch\` will record its own.`;
7948
+ }
7949
+ function assumedElsewhereMessage(facts, backups) {
7950
+ const others = [...new Set(backups.flatMap((backup) => backup.assumedInstalls))];
7951
+ return `The pristine backup(s) of claude ${facts.version} in ${backupDir()} have already been restored onto ${others.join(", ")} \u2014 not onto ${facts.binaryPath}. That was itself matched on the claude version in a file name, so it is not proof of ownership, but it is a reason not to hand the same bytes to a second install: two installs of one version are different files. Set TWEAKCC_CC_INSTALLATION_PATH=${others[0]} to restore that install, or reinstall Claude Code to make this one pristine.`;
7952
+ }
7863
7953
  function selectRestoreSource(facts) {
7864
7954
  const notes = [];
7955
+ let assumedForThisInstall = false;
7865
7956
  const recorded = facts.manifest;
7866
7957
  const speaksForThisVersion = !recorded?.claudeVersion || recorded.claudeVersion === facts.version;
7867
7958
  const manifest = recorded && recorded.binaryPath === facts.binaryPath && speaksForThisVersion ? recorded : null;
@@ -7873,10 +7964,46 @@ function selectRestoreSource(facts) {
7873
7964
  if (!chosen && manifest && identifiesABackup(manifest)) {
7874
7965
  return { action: "error", message: recordedBackupGoneMessage(facts, manifest) };
7875
7966
  }
7967
+ const manifestDescribesLiveBytes = !!manifest?.patchedSha256 && manifest.patchedSha256 === facts.liveSha256;
7968
+ if (chosen && !manifestDescribesLiveBytes) {
7969
+ const manifestChoice = chosen;
7970
+ const contradicting = facts.backups.filter(
7971
+ (backup) => backup.installs.includes(facts.binaryPath) && backup.sha256 !== manifestChoice.sha256
7972
+ );
7973
+ if (contradicting.length) {
7974
+ return {
7975
+ action: "error",
7976
+ message: manifestContradictsRecordMessage(facts, manifestChoice.path, contradicting)
7977
+ };
7978
+ }
7979
+ }
7980
+ if (!chosen) {
7981
+ const mine = facts.backups.filter((backup) => backup.installs.includes(facts.binaryPath));
7982
+ if (mine.length) {
7983
+ if (new Set(mine.map((backup) => backup.sha256)).size > 1) {
7984
+ return { action: "error", message: contradictoryProvenanceMessage(facts, mine) };
7985
+ }
7986
+ chosen = mine.find((backup) => backup.kind === "content-addressed") ?? mine[0];
7987
+ }
7988
+ }
7876
7989
  if (!chosen) {
7877
7990
  if (other) {
7878
7991
  return facts.backups.length ? { action: "error", message: otherInstallMessage(facts, other.binaryPath) } : { action: "error", message: noBackupMessage(facts) };
7879
7992
  }
7993
+ const recordedElsewhere = facts.backups.filter((backup) => backup.installs.length > 0);
7994
+ if (recordedElsewhere.length) {
7995
+ return { action: "error", message: otherInstallProvenanceMessage(facts, recordedElsewhere) };
7996
+ }
7997
+ const damaged = facts.backups.flatMap((backup) => backup.damagedProvenance);
7998
+ if (damaged.length) {
7999
+ return { action: "error", message: damagedProvenanceMessage(facts, damaged) };
8000
+ }
8001
+ const assumedElsewhere = facts.backups.filter(
8002
+ (backup) => backup.assumedInstalls.some((install) => install !== facts.binaryPath)
8003
+ );
8004
+ if (assumedElsewhere.length) {
8005
+ return { action: "error", message: assumedElsewhereMessage(facts, assumedElsewhere) };
8006
+ }
7880
8007
  const distinct = [...new Set(facts.backups.map((backup) => backup.sha256))];
7881
8008
  if (distinct.length > 1) {
7882
8009
  return {
@@ -7886,12 +8013,14 @@ function selectRestoreSource(facts) {
7886
8013
  }
7887
8014
  chosen = facts.backups.find((backup) => backup.kind === "content-addressed") ?? facts.backups[0];
7888
8015
  if (chosen) {
8016
+ assumedForThisInstall = true;
7889
8017
  notes.push(
7890
- `No patch manifest records ${facts.binaryPath}, so ${chosen.path} is being used as its pristine content on the strength of its claude ${facts.version} version tag alone. On a machine with more than one Claude Code install those bytes may belong to the other one \u2014 a clodex that had recorded this install would have refused rather than guess.`
8018
+ `Neither a patch manifest nor a provenance record ties ${chosen.path} to ${facts.binaryPath}, so it is being used as that install's pristine content on the strength of its claude ${facts.version} version tag alone \u2014 it predates the records clodex now writes. On a machine with more than one Claude Code install those bytes may belong to the other one, and clodex will not record this guess as provenance.`
7891
8019
  );
7892
8020
  }
7893
8021
  }
7894
8022
  if (!chosen) return { action: "error", message: noBackupMessage(facts) };
8023
+ if (manifest?.pristineProvenance === "assumed") assumedForThisInstall = true;
7895
8024
  return {
7896
8025
  action: "restore",
7897
8026
  backupPath: chosen.path,
@@ -7900,6 +8029,7 @@ function selectRestoreSource(facts) {
7900
8029
  // another version's binary, stored under a mislabeled name by an older
7901
8030
  // clodex. Executing it is the only evidence available; require it.
7902
8031
  probeVersion: chosen.kind === "legacy",
8032
+ assumedForThisInstall,
7903
8033
  notes
7904
8034
  };
7905
8035
  }
@@ -13800,7 +13930,7 @@ function readPatchManifest(path = getPatchManifestPath()) {
13800
13930
  }
13801
13931
  function writePatchManifest(manifest, path = getPatchManifestPath()) {
13802
13932
  mkdirSync5(getAppHome(), { recursive: true, mode: 448 });
13803
- writeFileSync5(path, `${JSON.stringify(manifest, null, 2)}
13933
+ writeFileSync6(path, `${JSON.stringify(manifest, null, 2)}
13804
13934
  `, { encoding: "utf8", mode: 384 });
13805
13935
  }
13806
13936
  function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
@@ -13958,7 +14088,7 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
13958
14088
  try {
13959
14089
  const fd = openSync7(lockPath, "wx");
13960
14090
  const content = { pid: process.pid, startedAt: now };
13961
- writeFileSync5(fd, JSON.stringify(content));
14091
+ writeFileSync6(fd, JSON.stringify(content));
13962
14092
  closeSync7(fd);
13963
14093
  return () => {
13964
14094
  try {
@@ -13983,10 +14113,19 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
13983
14113
  }
13984
14114
  return null;
13985
14115
  }
14116
+ var ignoredLaunchOverride = null;
14117
+ function takeIgnoredLaunchOverride() {
14118
+ const ignored = ignoredLaunchOverride;
14119
+ ignoredLaunchOverride = null;
14120
+ return ignored;
14121
+ }
13986
14122
  function resolveClaudeBinaryForPatch() {
13987
- const envOverride = process.env["TWEAKCC_CC_INSTALLATION_PATH"];
14123
+ const envOverride = process.env["TWEAKCC_CC_INSTALLATION_PATH"]?.trim() || null;
13988
14124
  const nativeSymlink = join8(homedir3(), ".local", "bin", "claude");
13989
- const source = envOverride?.trim() || (existsSync5(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
14125
+ if (envOverride && !existsSync5(envOverride)) {
14126
+ return { ok: false, reason: "patch-target-missing", declaredPath: envOverride };
14127
+ }
14128
+ const source = envOverride || (existsSync5(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
13990
14129
  if (!source) return { ok: false, reason: "binary-not-found" };
13991
14130
  let resolved;
13992
14131
  try {
@@ -14006,6 +14145,17 @@ function resolveClaudeBinaryForPatch() {
14006
14145
  };
14007
14146
  }
14008
14147
  resolved = followed.path;
14148
+ const launchOverride = process.env["CLODEX_CLAUDE_PATH"]?.trim() || null;
14149
+ let launchOverrideResolved = null;
14150
+ if (launchOverride) {
14151
+ try {
14152
+ const followedOverride = resolveThroughNpmShims(realpathSync2(launchOverride));
14153
+ launchOverrideResolved = followedOverride.ok ? followedOverride.path : realpathSync2(launchOverride);
14154
+ } catch {
14155
+ launchOverrideResolved = launchOverride;
14156
+ }
14157
+ }
14158
+ ignoredLaunchOverride = launchOverrideResolved && launchOverrideResolved !== resolved ? { used: resolved, ignored: launchOverride } : null;
14009
14159
  try {
14010
14160
  if (!statSync8(resolved).isFile()) return { ok: false, reason: "binary-not-found" };
14011
14161
  } catch {
@@ -14029,7 +14179,7 @@ function installScriptCommand(path) {
14029
14179
  }
14030
14180
  function describePatchTargetFailure(target, command = "patch") {
14031
14181
  if (target.reason === "launcher-unresolved") {
14032
- return `${target.shimPath} starts Claude Code but is not Claude Code itself, and clodex could not follow it: ${target.detail}. clodex will not patch a launcher script \u2014 that fails with "Unable to detect installation type". Set CLODEX_CLAUDE_PATH to the Claude Code program itself (for an npm install on Windows that is node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe under the directory holding the launcher), then run the command again.`;
14182
+ return `${target.shimPath} starts Claude Code but is not Claude Code itself, and clodex could not follow it: ${target.detail}. clodex will not patch a launcher script \u2014 that fails with "Unable to detect installation type". Set TWEAKCC_CC_INSTALLATION_PATH to the Claude Code program itself (for an npm install on Windows that is node_modules\\@anthropic-ai\\claude-code\\bin\\claude.exe under the directory holding the launcher), then run the command again.`;
14033
14183
  }
14034
14184
  if (target.reason === "native-binary-missing") {
14035
14185
  const installer = installScriptCommand(target.installScriptPath);
@@ -14038,6 +14188,9 @@ function describePatchTargetFailure(target, command = "patch") {
14038
14188
  const restore = command === "restore" ? ` If you need to restore by hand, pristine backups are in ${backupDir()}.` : "";
14039
14189
  return `${target.binaryPath} is Claude Code's npm placeholder, not its native binary, so the npm install is incomplete. ${remedy} ${retry}${restore} If this is a custom wrapper rather than the npm placeholder, set TWEAKCC_CC_INSTALLATION_PATH to the native Claude Code binary.`;
14040
14190
  }
14191
+ if (target.reason === "patch-target-missing") {
14192
+ return `TWEAKCC_CC_INSTALLATION_PATH is set to ${target.declaredPath}, which does not exist. clodex will not look for another Claude Code instead \u2014 patching or restoring a different install than the one you named is how one install's pristine bytes end up over another's. Point it at the Claude Code program, or unset it to let clodex find your install.`;
14193
+ }
14041
14194
  return target.reason === "binary-not-found" ? "claude binary not found. Install Claude Code or set TWEAKCC_CC_INSTALLATION_PATH." : `Could not determine the version of ${target.binaryPath} (\`claude --version\` failed). clodex will not patch a binary whose version it cannot read, because the version selects the pristine backup it patches from. If a previous patch left the install broken, \`clodex patch --restore\` still works \u2014 it reads the version from the patch manifest.`;
14042
14195
  }
14043
14196
  function summarizePatchResults(results) {
@@ -14060,14 +14213,15 @@ function verifyPristineSource(plan, version) {
14060
14213
  message: `Refusing to use ${plan.backupPath} as the pristine source for claude ${version}: it reports ${backupVersion ? `version ${backupVersion}` : "no version at all"}. That backup predates content-addressed backup names and does not hold this version's bytes. Remove it (or reinstall Claude Code), then run \`clodex patch\`.`
14061
14214
  };
14062
14215
  }
14063
- function publishBackupFile(from, to) {
14216
+ function publishFileByRename(from, to, mode) {
14064
14217
  const temp = `${to}.tmp-${process.pid}-${Date.now().toString(36)}`;
14065
14218
  try {
14066
14219
  copyFileSync(from, temp);
14067
- renameSync2(temp, to);
14220
+ if (mode !== void 0) chmodSync4(temp, mode);
14221
+ renameSync3(temp, to);
14068
14222
  } catch (err) {
14069
14223
  try {
14070
- rmSync(temp, { force: true });
14224
+ rmSync2(temp, { force: true });
14071
14225
  } catch {
14072
14226
  }
14073
14227
  throw err;
@@ -14131,6 +14285,7 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
14131
14285
  let patchedSha256;
14132
14286
  let backup;
14133
14287
  let pristineSha256;
14288
+ const pristine = { provenance: "established" };
14134
14289
  try {
14135
14290
  mkdirSync5(backupDir(), { recursive: true });
14136
14291
  const { tryDetectInstallation, readContent, writeContent } = await import("tweakcc");
@@ -14179,6 +14334,18 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
14179
14334
  }
14180
14335
  pristineSha256 = plan.pristineSha256;
14181
14336
  backup = plan.backupPath;
14337
+ const inheritsAGuess = (path) => {
14338
+ const existing = readInstallProvenance(installProvenancePath(path, binaryPath));
14339
+ return existing === "damaged" || existing !== null && existing.assumed;
14340
+ };
14341
+ const provenanceAssumed = plan.action === "restore" && plan.assumedForThisInstall || inheritsAGuess(plan.backupPath) || inheritsAGuess(contentAddressedBackupPath(version, plan.pristineSha256)) || inheritsAGuess(legacyBackupPath(version)) || facts.backups.some(
14342
+ (candidate) => candidate.sha256 === plan.pristineSha256 && candidate.assumedInstalls.includes(binaryPath)
14343
+ );
14344
+ const recordProvenance = (path) => {
14345
+ if (recordBackupProvenance(path, binaryPath, { assumed: provenanceAssumed }) === "assumed") {
14346
+ pristine.provenance = "assumed";
14347
+ }
14348
+ };
14182
14349
  if (!loaded) {
14183
14350
  loaded = await seedCandidate(backup);
14184
14351
  if (isPatchedClaudeSource(loaded.source)) {
@@ -14190,17 +14357,22 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
14190
14357
  `the patch candidate no longer matches the pristine bytes it was seeded from; refusing to publish it as ${plan.backupPath}`
14191
14358
  );
14192
14359
  }
14193
- publishBackupFile(candidatePath, plan.backupPath);
14360
+ recordProvenance(plan.backupPath);
14361
+ publishFileByRename(candidatePath, plan.backupPath);
14194
14362
  }
14195
14363
  const canonical = contentAddressedBackupPath(version, pristineSha256);
14196
14364
  if (canonical !== backup) {
14365
+ recordProvenance(backup);
14366
+ recordProvenance(canonical);
14197
14367
  const alreadyStored = facts.backups.some(
14198
14368
  (candidate) => candidate.path === canonical && candidate.sha256 === pristineSha256
14199
14369
  );
14200
- if (!alreadyStored) publishBackupFile(backup, canonical);
14370
+ if (!alreadyStored) publishFileByRename(backup, canonical);
14201
14371
  backup = canonical;
14202
14372
  }
14203
- publishBackupFile(backup, tweakccMirrorBackupPath());
14373
+ publishFileByRename(backup, tweakccMirrorBackupPath());
14374
+ recordProvenance(backup);
14375
+ if (plan.backupPath !== backup) recordProvenance(plan.backupPath);
14204
14376
  const builtIn = applyClodexPatches(loaded.source, desired.config);
14205
14377
  results = builtIn.results;
14206
14378
  const failedEffortPatches = requiredEffortPatchFailures(results);
@@ -14267,7 +14439,7 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
14267
14439
  else if (publishedBlob) resignMachOBinary(candidatePath);
14268
14440
  patchedSize = statSync8(candidatePath).size;
14269
14441
  patchedSha256 = sha256File(candidatePath);
14270
- renameSync2(candidatePath, binaryPath);
14442
+ renameSync3(candidatePath, binaryPath);
14271
14443
  } catch (err) {
14272
14444
  const detailLines = err instanceof PatchApplyError ? summarizePatchResults(err.results) : [];
14273
14445
  if (opts.trace && detailLines.length) {
@@ -14282,7 +14454,7 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
14282
14454
  } finally {
14283
14455
  if (candidateDir !== void 0) {
14284
14456
  try {
14285
- rmSync(candidateDir, { recursive: true, force: true });
14457
+ rmSync2(candidateDir, { recursive: true, force: true });
14286
14458
  } catch (err) {
14287
14459
  if (opts.trace) {
14288
14460
  process.stderr.write(
@@ -14305,6 +14477,7 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
14305
14477
  patchedSha256,
14306
14478
  backupPath: backup,
14307
14479
  pristineSha256,
14480
+ ...pristine.provenance === "assumed" ? { pristineProvenance: "assumed" } : {},
14308
14481
  patchedAt: (/* @__PURE__ */ new Date()).toISOString()
14309
14482
  };
14310
14483
  writePatchManifest(manifest);
@@ -14318,7 +14491,7 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
14318
14491
  };
14319
14492
  }
14320
14493
  function runRestoreCommand(target) {
14321
- if (!target.ok && (target.reason === "binary-not-found" || target.reason === "native-binary-missing")) {
14494
+ if (!target.ok && (target.reason === "binary-not-found" || target.reason === "patch-target-missing" || target.reason === "native-binary-missing")) {
14322
14495
  p2.log.error(describePatchTargetFailure(target, "restore"));
14323
14496
  return 1;
14324
14497
  }
@@ -14353,16 +14526,65 @@ function runRestoreCommand(target) {
14353
14526
  p2.log.error(verified.message);
14354
14527
  return 1;
14355
14528
  }
14356
- copyFileSync(plan.backupPath, binaryPath);
14529
+ if (manifest && manifest.backupPath && manifest.binaryPath === binaryPath && manifest.claudeVersion !== version && manifest.backupPath !== plan.backupPath) {
14530
+ try {
14531
+ if (existsSync5(manifest.backupPath)) {
14532
+ recordBackupProvenance(manifest.backupPath, manifest.binaryPath, {
14533
+ assumed: manifest.pristineProvenance === "assumed"
14534
+ });
14535
+ }
14536
+ } catch (err) {
14537
+ p2.log.warn(
14538
+ `Could not record in ${backupDir()} that ${manifest.backupPath} holds the pristine bytes of claude ${manifest.claudeVersion} at ${manifest.binaryPath} (${err instanceof Error ? err.message : String(err)}). That version may need to be reinstalled rather than restored.`
14539
+ );
14540
+ }
14541
+ }
14542
+ let recorded;
14357
14543
  try {
14358
- unlinkSync3(getPatchManifestPath());
14544
+ recorded = recordBackupProvenance(plan.backupPath, binaryPath, { assumed: plan.assumedForThisInstall });
14545
+ } catch (err) {
14546
+ recorded = "failed";
14547
+ const detail = err instanceof Error ? err.message : String(err);
14548
+ if (plan.assumedForThisInstall) {
14549
+ p2.log.error(
14550
+ `Refusing to restore ${binaryPath} from ${plan.backupPath}: nothing but the claude ${version} version tag ties those bytes to this install, and clodex cannot record that in ${backupDir()} (${detail}). Writing them without that record would leave the machine unable to tell afterwards that the match was a guess. Fix the backup directory, or reinstall Claude Code to make this install pristine.`
14551
+ );
14552
+ return 1;
14553
+ }
14554
+ p2.log.warn(
14555
+ `Could not record in ${backupDir()} that ${plan.backupPath} holds the pristine bytes of ${binaryPath} (${detail}). Restoring anyway and keeping the patch manifest, so that record is not lost \u2014 a later restore would otherwise have nothing tying that backup to this install.`
14556
+ );
14557
+ }
14558
+ let publishedMode;
14559
+ try {
14560
+ publishedMode = statSync8(binaryPath).mode;
14359
14561
  } catch {
14360
14562
  }
14563
+ try {
14564
+ publishFileByRename(plan.backupPath, binaryPath, publishedMode);
14565
+ } catch (err) {
14566
+ p2.log.warn(
14567
+ `Could not replace ${binaryPath} through a new file (${err instanceof Error ? err.message : String(err)}); writing the pristine bytes in place instead. If claude then fails to start with a code-signing error, copy the backup to a new file and move it over the binary by hand.`
14568
+ );
14569
+ copyFileSync(plan.backupPath, binaryPath);
14570
+ }
14571
+ if (recorded === "established" && (!manifest || manifest.binaryPath === binaryPath)) {
14572
+ try {
14573
+ unlinkSync3(getPatchManifestPath());
14574
+ } catch {
14575
+ }
14576
+ }
14361
14577
  p2.log.success(`Restored pristine claude ${version} from ${plan.backupPath}.`);
14362
14578
  return 0;
14363
14579
  }
14364
14580
  async function runPatchCommand(opts = {}) {
14365
14581
  const target = resolveClaudeBinaryForPatch();
14582
+ const ignoredOverride = takeIgnoredLaunchOverride();
14583
+ if (ignoredOverride) {
14584
+ p2.log.warn(
14585
+ `CLODEX_CLAUDE_PATH is set to ${ignoredOverride.ignored}, but it does not choose what gets patched \u2014 ${ignoredOverride.used} does. CLODEX_CLAUDE_PATH selects the claude that gets LAUNCHED; set TWEAKCC_CC_INSTALLATION_PATH to patch a specific install.`
14586
+ );
14587
+ }
14366
14588
  if (opts.restore) return runRestoreCommand(target);
14367
14589
  if (!target.ok) {
14368
14590
  p2.log.error(describePatchTargetFailure(target));
@@ -14405,6 +14627,19 @@ async function runPatchCommand(opts = {}) {
14405
14627
  p2.log.warn("Another clodex process is patching the claude binary right now \u2014 skipped.");
14406
14628
  return 1;
14407
14629
  }
14630
+ if (manifest && manifest.backupPath && (manifest.binaryPath !== binaryPath || manifest.claudeVersion !== version)) {
14631
+ try {
14632
+ if (existsSync5(manifest.backupPath)) {
14633
+ recordBackupProvenance(manifest.backupPath, manifest.binaryPath, {
14634
+ assumed: manifest.pristineProvenance === "assumed"
14635
+ });
14636
+ }
14637
+ } catch (err) {
14638
+ p2.log.warn(
14639
+ `Could not record in ${backupDir()} that ${manifest.backupPath} holds the pristine bytes of ${manifest.binaryPath} before replacing the patch manifest (${err instanceof Error ? err.message : String(err)}). That install may need to be reinstalled rather than restored.`
14640
+ );
14641
+ }
14642
+ }
14408
14643
  try {
14409
14644
  const outcome = await applyPatch(binaryPath, version, desired, configHash, {
14410
14645
  trace: opts.trace ?? false,
@@ -14794,9 +15029,9 @@ import {
14794
15029
  mkdirSync as mkdirSync6,
14795
15030
  openSync as openSync8,
14796
15031
  readFileSync as readFileSync11,
14797
- renameSync as renameSync3,
15032
+ renameSync as renameSync4,
14798
15033
  unlinkSync as unlinkSync4,
14799
- writeFileSync as writeFileSync6
15034
+ writeFileSync as writeFileSync7
14800
15035
  } from "fs";
14801
15036
  import { dirname as dirname6 } from "path";
14802
15037
  var JOURNAL_SCHEMA_VERSION = 1;
@@ -14928,13 +15163,13 @@ function writeJournalUnlocked(journal, path) {
14928
15163
  let fd;
14929
15164
  try {
14930
15165
  fd = openSync8(tmp, "wx", FILE_MODE4);
14931
- writeFileSync6(fd, `${JSON.stringify(journal, null, 2)}
15166
+ writeFileSync7(fd, `${JSON.stringify(journal, null, 2)}
14932
15167
  `);
14933
15168
  fsyncSync2(fd);
14934
15169
  closeSync8(fd);
14935
15170
  fd = void 0;
14936
15171
  assertRegistryWriteOwnership(path);
14937
- renameSync3(tmp, path);
15172
+ renameSync4(tmp, path);
14938
15173
  syncParentDirectory(path);
14939
15174
  } finally {
14940
15175
  if (fd !== void 0) closeSync8(fd);
@@ -18907,7 +19142,7 @@ import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
18907
19142
 
18908
19143
  // src/http-proxy/ca.ts
18909
19144
  import { randomBytes as randomBytes2 } from "crypto";
18910
- import { chmodSync as chmodSync4, existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
19145
+ import { chmodSync as chmodSync5, existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
18911
19146
  import { dirname as dirname7, join as join9, resolve as resolve3 } from "path";
18912
19147
  import forge from "node-forge";
18913
19148
  var CERT_DIR = "http-proxy";
@@ -18934,16 +19169,16 @@ function certPaths() {
18934
19169
  };
18935
19170
  }
18936
19171
  function writePrivate(path, value) {
18937
- writeFileSync7(path, value, { encoding: "utf8", mode: 384 });
18938
- chmodSync4(path, 384);
19172
+ writeFileSync8(path, value, { encoding: "utf8", mode: 384 });
19173
+ chmodSync5(path, 384);
18939
19174
  }
18940
19175
  function writePublic(path, value) {
18941
- writeFileSync7(path, value, { encoding: "utf8", mode: 420 });
18942
- chmodSync4(path, 420);
19176
+ writeFileSync8(path, value, { encoding: "utf8", mode: 420 });
19177
+ chmodSync5(path, 420);
18943
19178
  }
18944
19179
  function generateCertificates(paths) {
18945
19180
  mkdirSync7(paths.dir, { recursive: true, mode: 448 });
18946
- chmodSync4(paths.dir, 448);
19181
+ chmodSync5(paths.dir, 448);
18947
19182
  const caKeys = forge.pki.rsa.generateKeyPair(2048);
18948
19183
  const caCert = forge.pki.createCertificate();
18949
19184
  caCert.publicKey = caKeys.publicKey;