claude-nomad 0.32.1 → 0.32.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.32.2](https://github.com/funkadelic/claude-nomad/compare/v0.32.1...v0.32.2) (2026-05-30)
4
+
5
+
6
+ ### Fixed
7
+
8
+ * **remap:** reject path-traversal in path-map logical keys ([#190](https://github.com/funkadelic/claude-nomad/issues/190)) ([1526fbb](https://github.com/funkadelic/claude-nomad/commit/1526fbbbb7c6beb258d882c1c26cd45447ab226b))
9
+
10
+
11
+ ### Changed
12
+
13
+ * **sonar:** source projectVersion from package.json at scan time ([#188](https://github.com/funkadelic/claude-nomad/issues/188)) ([c00dd6a](https://github.com/funkadelic/claude-nomad/commit/c00dd6a5135a8753d56cf4165cf5b9782c9bde3e))
14
+
3
15
  ## [0.32.1](https://github.com/funkadelic/claude-nomad/compare/v0.32.0...v0.32.1) (2026-05-30)
4
16
 
5
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-nomad",
3
- "version": "0.32.1",
3
+ "version": "0.32.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": [
@@ -1,11 +1,40 @@
1
1
  import { NEVER_SYNC } from './config.ts';
2
+ import { NomadFatal } from './utils.ts';
3
+
4
+ /**
5
+ * `logical` keys in `path-map.json` are project identifiers (e.g. `ha-acwd`,
6
+ * `foo`), never path fragments. A crafted key like `../escape` or `foo/bar`
7
+ * would escape `shared/projects/` (or `shared/extras/`) via `join()` (which
8
+ * normalizes `..`) and land content somewhere unexpected on the filesystem.
9
+ * The push allow-list catches such commits at the `git add` boundary, but the
10
+ * filesystem mutation has already happened by then. This check fails fast
11
+ * before any write. The pattern matches what every reasonable project name
12
+ * looks like and rejects everything else.
13
+ */
14
+ const SAFE_LOGICAL = /^[A-Za-z0-9._-]+$/;
15
+
16
+ /**
17
+ * Throw `NomadFatal` unless `logical` is a path-separator-free project
18
+ * identifier (see `SAFE_LOGICAL`). Path-traversal defense-in-depth; called
19
+ * before any filesystem mutation by every remap and extras op that joins
20
+ * `logical` into a filesystem path.
21
+ *
22
+ * @param logical - A `path-map.json` projects key to validate.
23
+ */
24
+ export function assertSafeLogical(logical: string): void {
25
+ if (!SAFE_LOGICAL.test(logical) || logical === '.' || logical === '..') {
26
+ throw new NomadFatal(
27
+ `invalid logical name in path-map.json: ${JSON.stringify(logical)} (must match [A-Za-z0-9._-]+; no path separators or '..')`,
28
+ );
29
+ }
30
+ }
2
31
 
3
32
  /**
4
33
  * Single-segment path characters allowed in a `sharedDirs` entry. Mirrors
5
- * `SAFE_LOGICAL` in `extras-sync.guards.ts` but applied to global support
6
- * directory names rather than per-project logical names. Must match
7
- * `^[A-Za-z0-9._-]+$` so no path separator, no shell-special character, no
8
- * leading dot that would collide with a hidden state directory.
34
+ * `SAFE_LOGICAL` above but applied to global support directory names rather
35
+ * than per-project logical names. Must match `^[A-Za-z0-9._-]+$` so no path
36
+ * separator, no shell-special character, no leading dot that would collide
37
+ * with a hidden state directory.
9
38
  */
10
39
  const SAFE_SEGMENT = /^[A-Za-z0-9._-]+$/;
11
40
 
@@ -2,30 +2,7 @@ import { isAbsolute, normalize } from 'node:path';
2
2
 
3
3
  import { NomadFatal } from './utils.ts';
4
4
 
5
- /**
6
- * `logical` keys in `path-map.json` are project identifiers (e.g. `ha-acwd`,
7
- * `foo`), never path fragments. A crafted key like `../escape` or `foo/bar`
8
- * would escape `shared/extras/` via `join()` (which normalizes `..`) and land
9
- * content somewhere unexpected on the filesystem. The push allow-list catches
10
- * such commits at the `git add` boundary, but the filesystem mutation has
11
- * already happened by then. This check fails fast before any write. The
12
- * pattern matches what every reasonable project name looks like and rejects
13
- * everything else; tighten only if a real project needs broader characters.
14
- */
15
- const SAFE_LOGICAL = /^[A-Za-z0-9._-]+$/;
16
-
17
- /**
18
- * Throw `NomadFatal` unless `logical` is a path-separator-free project
19
- * identifier (see `SAFE_LOGICAL`). Path-traversal defense-in-depth; called
20
- * before any filesystem mutation by every extras op.
21
- */
22
- export function assertSafeLogical(logical: string): void {
23
- if (!SAFE_LOGICAL.test(logical) || logical === '.' || logical === '..') {
24
- throw new NomadFatal(
25
- `invalid logical name in path-map.json extras: ${JSON.stringify(logical)} (must match [A-Za-z0-9._-]+; no path separators or '..')`,
26
- );
27
- }
28
- }
5
+ export { assertSafeLogical } from './config.sharedDirs.guard.ts';
29
6
 
30
7
  /**
31
8
  * Reject `localRoot` values that contain unnormalized segments (`..`,
package/src/remap.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
2
2
  import { join, relative, sep } from 'node:path';
3
3
 
4
+ import { assertSafeLogical } from './config.sharedDirs.guard.ts';
4
5
  import { CLAUDE_HOME, HOST, REPO_HOME, type PathMap } from './config.ts';
5
6
  import { die, log } from './utils.ts';
6
7
  import { backupBeforeWrite, backupRepoWrite } from './utils.fs.ts';
@@ -81,6 +82,7 @@ export function remapPull(
81
82
  if (!dryRun) mkdirSync(localProjects, { recursive: true });
82
83
 
83
84
  for (const [logical, hosts] of Object.entries(map.projects)) {
85
+ assertSafeLogical(logical);
84
86
  const localPath = hosts[HOST];
85
87
  if (!localPath || localPath === 'TBD') {
86
88
  unmapped++;
@@ -126,6 +128,7 @@ function buildReverseMap(map: PathMap): Map<string, string> {
126
128
  const reverse = new Map<string, string>();
127
129
  const encodedPaths = new Map<string, string>();
128
130
  for (const [logical, hosts] of Object.entries(map.projects)) {
131
+ assertSafeLogical(logical);
129
132
  const p = hosts[HOST];
130
133
  if (!p || p === 'TBD') continue;
131
134
  const encoded = encodePath(p);