claude-nomad 0.53.1 → 0.53.2

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/.gitleaks.toml CHANGED
@@ -14,10 +14,14 @@ useDefault = true
14
14
  # context anywhere in the repo. Scoping to `shared/projects/<logical>/.../*.jsonl`
15
15
  # with `condition = "AND"` (matching every other block below) confines the
16
16
  # suppression to synced transcripts: a real secret in a source file still fires.
17
+ # The Sonar-issue-key regex is length-bounded to the real key shape (`AY` + 17-22
18
+ # base64url chars = 19-24 total, matching the sibling `key:`/`rule:` block) and
19
+ # token-anchored with `\b` on both ends, so it cannot suppress a longer or
20
+ # embedded `AY`-prefixed credential as a substring (an unbounded `{20,}` could).
17
21
  [[allowlists]]
18
22
  description = "claude-nomad: structurally-distinguishable tool-output noise in synced session transcripts"
19
23
  regexes = [
20
- '''AY[A-Za-z0-9_-]{20,}''',
24
+ '''\bAY[A-Za-z0-9_-]{17,22}\b''',
21
25
  '''[\w./-]+\.[A-Za-z0-9]+:[\w-]+:\d+''',
22
26
  '''"id"\s*:\s*"[a-f0-9]{40,64}"''',
23
27
  '''key=[a-f0-9]{8,} [\w./-]+\.\w+:\d+''',
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.53.2](https://github.com/funkadelic/claude-nomad/compare/v0.53.1...v0.53.2) (2026-06-26)
4
+
5
+
6
+ ### Fixed
7
+
8
+ * **security:** close pull-side path-write and push-side scan-bypass vectors ([#339](https://github.com/funkadelic/claude-nomad/issues/339)) ([99f0320](https://github.com/funkadelic/claude-nomad/commit/99f0320d1181333fc00ea983fda7e675377def47))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * bump actions/checkout from 6.0.3 to 7.0.0 ([#336](https://github.com/funkadelic/claude-nomad/issues/336)) ([6df79cf](https://github.com/funkadelic/claude-nomad/commit/6df79cfcfe9bd5eaa72130d15fdcb074019822a8))
14
+ * bump the dev-dependencies group with 3 updates ([#337](https://github.com/funkadelic/claude-nomad/issues/337)) ([dd5a8ea](https://github.com/funkadelic/claude-nomad/commit/dd5a8eaffe70fe96f9d6f0921eaead8c3caf323b))
15
+
3
16
  ## [0.53.1](https://github.com/funkadelic/claude-nomad/compare/v0.53.0...v0.53.1) (2026-06-21)
4
17
 
5
18
 
package/dist/nomad.mjs CHANGED
@@ -42,7 +42,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
42
42
  ));
43
43
 
44
44
  // src/config.never-sync.ts
45
- var NEVER_SYNC, CLAUDE_EXTRA_NEVER_SYNC;
45
+ function isSecretFileName(name) {
46
+ return SECRET_FILE_PATTERNS.some((re) => re.test(name));
47
+ }
48
+ function isDeniedName(blockSet, name) {
49
+ return blockSet.has(name) || blockSet.has(name.toLowerCase()) || isSecretFileName(name);
50
+ }
51
+ var NEVER_SYNC, CLAUDE_EXTRA_NEVER_SYNC, SECRET_FILE_PATTERNS;
46
52
  var init_config_never_sync = __esm({
47
53
  "src/config.never-sync.ts"() {
48
54
  "use strict";
@@ -72,6 +78,20 @@ var init_config_never_sync = __esm({
72
78
  "sessions"
73
79
  ]);
74
80
  CLAUDE_EXTRA_NEVER_SYNC = /* @__PURE__ */ new Set([...NEVER_SYNC, "projects"]);
81
+ SECRET_FILE_PATTERNS = [
82
+ /^\.env(\..+)?$/i,
83
+ // .env, .env.local, .env.production
84
+ /\.pem$/i,
85
+ /\.key$/i,
86
+ /\.p12$/i,
87
+ /\.pfx$/i,
88
+ /^id_(rsa|dsa|ecdsa|ed25519)$/i,
89
+ /^\.netrc$/i,
90
+ /^\.npmrc$/i,
91
+ /^\.pgpass$/i,
92
+ /^\.git-credentials$/i,
93
+ /^credentials$/i
94
+ ];
75
95
  }
76
96
  });
77
97
 
@@ -667,6 +687,37 @@ function resolveTomlPath(repo = repoHome()) {
667
687
  const bundled = fileURLToPath(new URL("../.gitleaks.toml", import.meta.url));
668
688
  return existsSync13(bundled) ? bundled : null;
669
689
  }
690
+ function assertOverlayAllowlistsScoped(overlayBody) {
691
+ if (OVERLAY_INLINE_ALLOWLIST_RE.test(overlayBody)) {
692
+ throw new NomadFatal(
693
+ ".gitleaks.overlay.toml must declare allowlists as [[allowlists]] table blocks, not the dotted-key (allowlist.x = ...) or inline-table (allowlist = { ... }) form, which bypasses path-scope validation. Use a [[allowlists]] block with a `paths` entry."
694
+ );
695
+ }
696
+ let inAllowlist = false;
697
+ let hasPaths = false;
698
+ const closeBlock = () => {
699
+ if (inAllowlist && !hasPaths) {
700
+ throw new NomadFatal(
701
+ ".gitleaks.overlay.toml allowlist must be path-scoped: every [[allowlists]] block needs a `paths` entry so it cannot suppress findings repo-wide. Anchor `paths` to the files you intend to allow (e.g. shared/projects/<logical>/...jsonl)."
702
+ );
703
+ }
704
+ };
705
+ for (const line of overlayBody.split("\n")) {
706
+ if (TABLE_HEADER_RE.test(line)) {
707
+ closeBlock();
708
+ inAllowlist = OVERLAY_ALLOWLIST_HEADER_RE.test(line);
709
+ hasPaths = false;
710
+ } else if (inAllowlist) {
711
+ if (PATHS_KEY_RE.test(line)) hasPaths = true;
712
+ if (CATCH_ALL_RE.test(line)) {
713
+ throw new NomadFatal(
714
+ ".gitleaks.overlay.toml allowlist contains a catch-all pattern (.* or .+) that would suppress every finding. Replace it with a specific, anchored pattern."
715
+ );
716
+ }
717
+ }
718
+ }
719
+ closeBlock();
720
+ }
670
721
  function buildOverlayTempConfig(overlayBody, bundled) {
671
722
  const tempBody = `[extend]
672
723
  path = ${JSON.stringify(bundled)}
@@ -701,6 +752,7 @@ function resolveTomlConfig() {
701
752
  ".gitleaks.overlay.toml must not contain an [extend] block; it is generated automatically. Remove the [extend] section and retry."
702
753
  );
703
754
  }
755
+ assertOverlayAllowlistsScoped(overlayBody);
704
756
  const { configPath, tempPath } = buildOverlayTempConfig(overlayBody, bundled);
705
757
  return { path: configPath, tempPath };
706
758
  } catch (err) {
@@ -711,13 +763,18 @@ function resolveTomlConfig() {
711
763
  return { path: bundled, tempPath: null };
712
764
  }
713
765
  }
714
- var OVERLAY_EXTEND_RE;
766
+ var OVERLAY_EXTEND_RE, TABLE_HEADER_RE, OVERLAY_ALLOWLIST_HEADER_RE, OVERLAY_INLINE_ALLOWLIST_RE, PATHS_KEY_RE, CATCH_ALL_RE;
715
767
  var init_push_gitleaks_config = __esm({
716
768
  "src/push-gitleaks.config.ts"() {
717
769
  "use strict";
718
770
  init_config();
719
771
  init_utils();
720
772
  OVERLAY_EXTEND_RE = /^\s*(?:\[\s*extend\s*\]|extend\s*[.=])/m;
773
+ TABLE_HEADER_RE = /^\s*\[/;
774
+ OVERLAY_ALLOWLIST_HEADER_RE = /^\s*\[\[?\s*allowlists?\s*\]\]?/;
775
+ OVERLAY_INLINE_ALLOWLIST_RE = /^[ \t]*allowlists?[ \t]*[.=]/m;
776
+ PATHS_KEY_RE = /^\s*paths\s*=/;
777
+ CATCH_ALL_RE = /('''|"""|'|")\^?\(?\.[*+]\)?\$?\1/;
721
778
  }
722
779
  });
723
780
 
@@ -2700,12 +2757,31 @@ init_config();
2700
2757
 
2701
2758
  // src/remap.ts
2702
2759
  init_config_sharedDirs_guard();
2760
+ import { cpSync as cpSync4, existsSync as existsSync17, mkdirSync as mkdirSync4, readdirSync as readdirSync6, renameSync as renameSync3, rmSync as rmSync7, statSync as statSync4 } from "node:fs";
2761
+ import { join as join22, relative as relative3, sep as sep3 } from "node:path";
2762
+
2763
+ // src/extras-sync.guards.ts
2764
+ init_utils();
2765
+ init_config_sharedDirs_guard();
2766
+ import { isAbsolute, normalize } from "node:path";
2767
+ function assertSafeLocalRoot(localRoot, logical) {
2768
+ if (!isAbsolute(localRoot)) {
2769
+ throw new NomadFatal(
2770
+ `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
2771
+ );
2772
+ }
2773
+ if (localRoot !== normalize(localRoot)) {
2774
+ throw new NomadFatal(
2775
+ `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
2776
+ );
2777
+ }
2778
+ }
2779
+
2780
+ // src/remap.ts
2703
2781
  init_config();
2704
2782
  init_utils();
2705
2783
  init_utils_fs();
2706
2784
  init_utils_json();
2707
- import { cpSync as cpSync4, existsSync as existsSync17, mkdirSync as mkdirSync4, readdirSync as readdirSync6, renameSync as renameSync3, rmSync as rmSync7, statSync as statSync4 } from "node:fs";
2708
- import { join as join22, relative as relative3, sep as sep3 } from "node:path";
2709
2785
  var TMP_SUFFIX = ".nomad-tmp";
2710
2786
  function atomicMirror(src, dst, options) {
2711
2787
  const tmp = `${dst}${TMP_SUFFIX}`;
@@ -2760,6 +2836,7 @@ function remapPull(ts, opts = {}) {
2760
2836
  unmapped++;
2761
2837
  continue;
2762
2838
  }
2839
+ assertSafeLocalRoot(localPath, logical);
2763
2840
  const src = join22(repoProjects, logical);
2764
2841
  if (!existsSync17(src)) continue;
2765
2842
  const dst = join22(localProjects, encodePath(localPath));
@@ -3530,7 +3607,7 @@ import { createInterface as createInterface2 } from "node:readline/promises";
3530
3607
  // src/commands.push.recovery.actions.ts
3531
3608
  init_config();
3532
3609
  import { readFileSync as readFileSync12 } from "node:fs";
3533
- import { isAbsolute, resolve as resolve3, sep as sep5 } from "node:path";
3610
+ import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep5 } from "node:path";
3534
3611
 
3535
3612
  // src/commands.push.recovery.redact.ts
3536
3613
  init_config();
@@ -3937,7 +4014,7 @@ function makeDefaultReadLine(repo) {
3937
4014
  try {
3938
4015
  const repoRoot = resolve3(repo);
3939
4016
  const target = resolve3(repoRoot, file);
3940
- if (isAbsolute(file) || target !== repoRoot && !target.startsWith(repoRoot + sep5)) {
4017
+ if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep5)) {
3941
4018
  return null;
3942
4019
  }
3943
4020
  const content = readFileSync12(target, "utf8");
@@ -4846,25 +4923,6 @@ function listDivergingFiles(a, b) {
4846
4923
  init_config();
4847
4924
  import { cpSync as cpSync6, existsSync as existsSync32, lstatSync as lstatSync9, readdirSync as readdirSync11, rmSync as rmSync11 } from "node:fs";
4848
4925
  import { basename, join as join38 } from "node:path";
4849
-
4850
- // src/extras-sync.guards.ts
4851
- init_utils();
4852
- init_config_sharedDirs_guard();
4853
- import { isAbsolute as isAbsolute2, normalize } from "node:path";
4854
- function assertSafeLocalRoot(localRoot, logical) {
4855
- if (!isAbsolute2(localRoot)) {
4856
- throw new NomadFatal(
4857
- `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
4858
- );
4859
- }
4860
- if (localRoot !== normalize(localRoot)) {
4861
- throw new NomadFatal(
4862
- `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
4863
- );
4864
- }
4865
- }
4866
-
4867
- // src/extras-sync.core.ts
4868
4926
  init_utils();
4869
4927
  init_utils_json();
4870
4928
  function loadValidatedExtras(opts) {
@@ -4902,13 +4960,28 @@ function* eachExtrasTarget(v, counts) {
4902
4960
  }
4903
4961
  }
4904
4962
  }
4963
+ function stripCollidingDstSymlinks(src, dst, isExcluded) {
4964
+ if (!existsSync32(dst)) return;
4965
+ for (const name of readdirSync11(src)) {
4966
+ if (isExcluded(name)) continue;
4967
+ const dstPath = join38(dst, name);
4968
+ const dstStat = lstatSync9(dstPath, { throwIfNoEntry: false });
4969
+ if (dstStat === void 0) continue;
4970
+ if (dstStat.isSymbolicLink()) {
4971
+ rmSync11(dstPath, { recursive: true, force: true });
4972
+ } else if (dstStat.isDirectory() && lstatSync9(join38(src, name)).isDirectory()) {
4973
+ stripCollidingDstSymlinks(join38(src, name), dstPath, isExcluded);
4974
+ }
4975
+ }
4976
+ }
4905
4977
  function copyExtrasOverlayFiltered(src, dst, blockSet) {
4978
+ stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
4906
4979
  try {
4907
4980
  cpSync6(src, dst, {
4908
4981
  recursive: true,
4909
4982
  force: true,
4910
4983
  verbatimSymlinks: true,
4911
- filter: (srcEntry) => srcEntry === src || !blockSet.has(basename(srcEntry))
4984
+ filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
4912
4985
  });
4913
4986
  } catch (err) {
4914
4987
  const e = err;
@@ -4933,12 +5006,12 @@ function copyExtrasFiltered(src, dst, blockSet) {
4933
5006
  recursive: true,
4934
5007
  force: true,
4935
5008
  verbatimSymlinks: true,
4936
- filter: (srcEntry) => srcEntry === src || !blockSet.has(basename(srcEntry))
5009
+ filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
4937
5010
  });
4938
5011
  }
4939
5012
  function prunePreservingDenied(src, dst, blockSet) {
4940
5013
  for (const name of readdirSync11(dst)) {
4941
- if (blockSet.has(name)) continue;
5014
+ if (isDeniedName(blockSet, name)) continue;
4942
5015
  const dstPath = join38(dst, name);
4943
5016
  const srcStat = lstatSync9(join38(src, name), { throwIfNoEntry: false });
4944
5017
  if (srcStat === void 0) {
@@ -4959,11 +5032,12 @@ function copyExtrasFilteredPreserving(src, dst, blockSet) {
4959
5032
  if (dstStat.isDirectory()) prunePreservingDenied(src, dst, blockSet);
4960
5033
  else rmSync11(dst, { recursive: true, force: true });
4961
5034
  }
5035
+ stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
4962
5036
  cpSync6(src, dst, {
4963
5037
  recursive: true,
4964
5038
  force: true,
4965
5039
  verbatimSymlinks: true,
4966
- filter: (srcEntry) => srcEntry === src || !blockSet.has(basename(srcEntry))
5040
+ filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
4967
5041
  });
4968
5042
  }
4969
5043
  function prunePreservingBy(src, dst, isPreserved) {
@@ -4989,6 +5063,7 @@ function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
4989
5063
  if (dstStat.isDirectory()) prunePreservingBy(src, dst, isPreserved);
4990
5064
  else rmSync11(dst, { recursive: true, force: true });
4991
5065
  }
5066
+ stripCollidingDstSymlinks(src, dst, isPreserved);
4992
5067
  cpSync6(src, dst, {
4993
5068
  recursive: true,
4994
5069
  force: true,
@@ -5266,7 +5341,7 @@ function isGsdOwned(name) {
5266
5341
  return name.startsWith(GSD_PREFIX);
5267
5342
  }
5268
5343
  function isSkillExcluded(name) {
5269
- return isGsdOwned(name) || ALWAYS_NEVER_SYNC.has(name);
5344
+ return isGsdOwned(name) || isDeniedName(ALWAYS_NEVER_SYNC, name);
5270
5345
  }
5271
5346
  function copySkillsPush(src, dst) {
5272
5347
  const srcNames = readdirSync13(src, { encoding: "utf8" });
@@ -5925,7 +6000,7 @@ function isNeverSync(path) {
5925
6000
  const blockSet = blockSetFor(segments);
5926
6001
  const scan = segments[0] === "shared" && segments[1] === "extras" ? segments.slice(4) : segments;
5927
6002
  for (const segment of scan) {
5928
- if (blockSet.has(segment)) return true;
6003
+ if (isDeniedName(blockSet, segment)) return true;
5929
6004
  }
5930
6005
  return false;
5931
6006
  }
@@ -6843,7 +6918,7 @@ function parsePushArgs(argv) {
6843
6918
  // package.json
6844
6919
  var package_default = {
6845
6920
  name: "claude-nomad",
6846
- version: "0.53.1",
6921
+ version: "0.53.2",
6847
6922
  type: "module",
6848
6923
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
6849
6924
  keywords: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-nomad",
3
- "version": "0.53.1",
3
+ "version": "0.53.2",
4
4
  "type": "module",
5
5
  "description": "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
6
6
  "keywords": [