claude-nomad 0.32.1 → 0.32.3
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 +19 -0
- package/package.json +1 -1
- package/src/commands.push.allowlist.ts +16 -9
- package/src/config.sharedDirs.guard.ts +33 -4
- package/src/config.ts +20 -12
- package/src/extras-sync.guards.ts +1 -24
- package/src/remap.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.32.3](https://github.com/funkadelic/claude-nomad/compare/v0.32.2...v0.32.3) (2026-05-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
* **push:** hard-block sensitive never-sync files under extras ([#191](https://github.com/funkadelic/claude-nomad/issues/191)) ([6509387](https://github.com/funkadelic/claude-nomad/commit/6509387b5724d13e8aa2122eb99cdd80e58da2ee))
|
|
9
|
+
|
|
10
|
+
## [0.32.2](https://github.com/funkadelic/claude-nomad/compare/v0.32.1...v0.32.2) (2026-05-30)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
* **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))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
* **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))
|
|
21
|
+
|
|
3
22
|
## [0.32.1](https://github.com/funkadelic/claude-nomad/compare/v0.32.0...v0.32.1) (2026-05-30)
|
|
4
23
|
|
|
5
24
|
|
package/package.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ALWAYS_NEVER_SYNC,
|
|
3
|
+
NEVER_SYNC,
|
|
4
|
+
PUSH_ALLOWED_STATIC,
|
|
5
|
+
SUPPORTED_EXTRAS,
|
|
6
|
+
type PathMap,
|
|
7
|
+
} from './config.ts';
|
|
2
8
|
import { isValidSharedDir } from './config.sharedDirs.guard.ts';
|
|
3
9
|
import { fail, NomadFatal } from './utils.ts';
|
|
4
10
|
|
|
@@ -22,17 +28,18 @@ function isAllowed(path: string, allowed: readonly string[]): boolean {
|
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/**
|
|
25
|
-
* True when any path segment matches a
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
+
* True when any path segment matches a hard-block entry. Outside the extras
|
|
32
|
+
* tree the full `NEVER_SYNC` set applies. Inside `shared/extras/` only the
|
|
33
|
+
* `ALWAYS_NEVER_SYNC` subset applies (Pitfall 6): the broader set was authored
|
|
34
|
+
* against `~/.claude/` semantics for ephemeral state, so `.planning/todos/` and
|
|
35
|
+
* similar legitimate GSD content must pass, but genuinely-sensitive host-local
|
|
36
|
+
* files (`.credentials.json`, `settings.local.json`, `.claude.json`, ...) stay
|
|
37
|
+
* blocked even when nested inside a synced extras dir.
|
|
31
38
|
*/
|
|
32
39
|
function isNeverSync(path: string): boolean {
|
|
33
|
-
|
|
40
|
+
const blockSet = path.startsWith('shared/extras/') ? ALWAYS_NEVER_SYNC : NEVER_SYNC;
|
|
34
41
|
for (const segment of path.split('/')) {
|
|
35
|
-
if (
|
|
42
|
+
if (blockSet.has(segment)) return true;
|
|
36
43
|
}
|
|
37
44
|
return false;
|
|
38
45
|
}
|
|
@@ -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`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
package/src/config.ts
CHANGED
|
@@ -106,19 +106,28 @@ export function allSharedLinks(map: PathMap): string[] {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* Whitelist of names allowed in `path-map.json
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* named-extras opt-in mechanism: only entries appearing in this list are
|
|
114
|
-
* eligible for sync. Widening to include `.notes`, `.scratch`, `AGENTS.md`,
|
|
115
|
-
* etc. is a one-line edit here with no schema migration required (the field
|
|
116
|
-
* is additive on the consumer side). Mirrors `SHARED_LINKS` in shape and
|
|
117
|
-
* intent: a short, append-only `as const` tuple that downstream callers
|
|
118
|
-
* narrow against.
|
|
109
|
+
* Whitelist of names allowed in the `extras` field of `path-map.json`. Each
|
|
110
|
+
* entry is a directory (e.g. `.planning`) or root-level file (`CLAUDE.md`)
|
|
111
|
+
* copied under `shared/extras/<logical>/<name>`. Only listed names are
|
|
112
|
+
* eligible for sync; widening is a one-line edit with no migration required.
|
|
119
113
|
*/
|
|
120
114
|
export const SUPPORTED_EXTRAS = ['.planning', 'CLAUDE.md'] as const;
|
|
121
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Credential and host-config file names blocked even under `shared/extras/`,
|
|
118
|
+
* where the broader `NEVER_SYNC` segment scan is narrowed to avoid
|
|
119
|
+
* false-blocking ephemeral dir names (`todos`, `plans`, etc.) inside synced
|
|
120
|
+
* `.planning/` trees (Pitfall 6). Strict subset of `NEVER_SYNC`; doctor
|
|
121
|
+
* display and sharedDirs guard use the full set.
|
|
122
|
+
*/
|
|
123
|
+
export const ALWAYS_NEVER_SYNC = new Set([
|
|
124
|
+
'.claude.json',
|
|
125
|
+
'.credentials.json',
|
|
126
|
+
'settings.local.json',
|
|
127
|
+
'history.jsonl',
|
|
128
|
+
'stats-cache.json',
|
|
129
|
+
]);
|
|
130
|
+
|
|
122
131
|
/**
|
|
123
132
|
* Path segments that must never cross the sync boundary in either direction.
|
|
124
133
|
* Defense-in-depth pair with `PUSH_ALLOWED_STATIC`: even if the allow-list
|
|
@@ -142,8 +151,7 @@ export const NEVER_SYNC = new Set([
|
|
|
142
151
|
'statsig',
|
|
143
152
|
'telemetry',
|
|
144
153
|
'ide',
|
|
145
|
-
// Host-local caches and runtime state
|
|
146
|
-
// so the sharedDirs guard rejects an accidental opt-in.
|
|
154
|
+
// Host-local caches and runtime state (sharedDirs guard also rejects these).
|
|
147
155
|
'cache',
|
|
148
156
|
'backups',
|
|
149
157
|
'paste-cache',
|
|
@@ -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);
|