claudeos-core 2.5.1 → 2.5.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
@@ -4,6 +4,7 @@
4
4
 
5
5
  Quick navigation to recent releases:
6
6
 
7
+ - [`2.5.2`](#252--2026-09-09) — Safety patch: URL passwords the masking rule cannot reach are dropped whole (raw `/` `?` `#` space, digit-leading, `@` in a query string), `host`/`apiTarget` never carry the sentinel, `init` names the dropped keys. Scanner byte-identical to 2.5.1.
7
8
  - [`2.5.1`](#251--2026-09-08) — Legacy JVM detection (non-Boot Spring 1.x–6.x, Ant / Ivy, Eclipse / IntelliJ / NetBeans metadata, `WEB-INF/lib` jars, `web.xml`, Maven multi-module and sibling projects, eGovFrame), `.env` key-name redaction gaps closed, `.env.example` DB detection, LF line endings
8
9
  - [`2.5.0`](#250--2026-09-07) — Correctness release: append-only `decision-log.md`, read-only `health`, user-owned rules survive `--force`, flat-layout Java detection, Java multi-module + Next.js route groups, orchestrator-written path allowlist, `.env` credential masking
9
10
  - [`2.4.4`](#244--2026-05-04) — Translation polish for `docs/{lang}/` × 8 non-Korean languages + broken `#quick-start` anchor fix
@@ -28,6 +29,49 @@ For older entries scroll past v1.5.0 or use the GitHub blame view.
28
29
 
29
30
  ---
30
31
 
32
+ ## [2.5.2] — 2026-09-09
33
+
34
+ Safety patch. v2.5.0 introduced URL credential masking and v2.5.1 closed its key-name gaps; this release closes the shapes the masking rule itself cannot reach. **Scope is deliberately narrow.** `plan-installer/scanners/scan-java.js` and `plan-installer/stack-detector.js` are **byte-identical to v2.5.1**, so no detection, domain or counting behavior changes at all. The Java layout work that was originally staged for this version (eGovFrame `web/` HTTP layers, `*/impl/` sub-layers, directory-fallback corrections) is withdrawn and deferred: independent review reproduced hard failures in it, including a scanner crash and a zero-domain `init` abort on trees v2.5.1 handled correctly. Shipping the credential fixes on their own is worth more than shipping them attached to a scanner regression.
35
+
36
+ `project-analysis.json` gains one optional array (`stack.envInfo.credentialWarnings`); two existing fields (`stack.envInfo.host`, `stack.envInfo.apiTarget`) can now be `null` where they previously carried a sentinel string. Test suite: 974 → **981 / 981** pass (7 added).
37
+
38
+ ### Fixed — safety
39
+
40
+ - **A URL password the userinfo rule cannot rewrite is no longer written verbatim.** v2.5.0 masks userinfo between `://` and the last `@` of the authority, and deliberately does not rewrite an `@` that appears after the first `/`, because such an `@` normally belongs to the path (`https://cdn.example.com/npm/@scope/pkg`) and rewriting it would replace the real host. `postgres://app:pa/ss@db:5432/app` therefore passed through unchanged, and because its key is `DATABASE_URL` the key-name rule did not backstop it either. That was the remaining combination able to put a plaintext password into `project-analysis.json`, which the Pass 3/4 prompts instruct the model to read. v2.5.1 recorded it as an accepted limit on the grounding that such a value is not a valid URL; the base64 alphabet contains `/`, so generated passwords hit the shape routinely.
41
+
42
+ `hasUnmaskedUrlCredentials` locates the `@` that actually terminates an authority: the last one followed by something that can be a host, optionally `:port`, then a delimiter or end of value. If the text before it holds no `/`, the userinfo rule already handled the value. If it does, the real authority ran past the point where that rule was forced to stop, and the **whole value** is dropped (`***REDACTED***`) rather than partially rewritten. Once the authority is ambiguous the host cannot be located reliably, and a lost host is a far cheaper failure than a leaked credential.
43
+
44
+ Three shapes beyond the original `/` case are caught. A password **beginning with digits** (`postgres://user:12345/6@db/app`) defeats any check that reasons from the truncated authority, since `user:12345` reads as a valid `host:port`. A password holding a **space** is invisible to the userinfo rule, whose character class excludes whitespace. And a DSN that also carries an `@` **inside a query parameter** — an email address, a redirect URL — must not end the search early; the scan continues past that `@` to the earlier one that terminates the real authority.
45
+
46
+ `user:12345` and `a.com:8080` are syntactically indistinguishable, so where the ambiguity is irreducible the scheme breaks the tie: for a DSN the value is treated as credentials, for `http`/`https`/`ws`/`wss` — where an `@` inside a path is routine — a head that is already a complete `host[:port]` means the authority ended there. Three further guards keep credential-free values intact: a bracketed IPv6 literal can only be a host, an `@` that sits in a query string after a path has begun is query content, and an unexpanded `${VAR}` template holds no live secret. Both corpora are pinned as tests: thirteen flagged shapes and twenty-six that must not flag.
47
+
48
+ - **`envInfo.host` and `envInfo.apiTarget` never carry the sentinel.** Both are rendered straight into CLAUDE.md §3, and the scaffold's only sentinel instruction covers `envInfo.vars`, so a dropped value reached the generated document as a literal `| API Proxy Target | ***REDACTED*** |` row. They are `null` instead, which the scaffold already handles by omitting the row.
49
+
50
+ - **`init` says which key it dropped.** Losing a `DATABASE_URL` silently looks like a detection bug. Phase 1 now names the affected keys — key names only, never any part of the value — for the root `.env` **and** for a sub-directory SPA's own `.env`, which the first cut of this warning did not read.
51
+
52
+ ### Withdrawn
53
+
54
+ - The Java layout work staged for v2.5.2 is not in this release. Independent review of it reproduced, against v2.5.1 as the baseline: a `TypeError` crash out of `scanJavaDomains` on a class name containing `Constructor`; a zero-domain result that aborts `init` on a single-domain legacy tree v2.5.1 scanned correctly; and five shapes that silently dropped real domains or their files, including numbered eGovFrame controllers (`EgovSample2Controller`) and any domain whose package name appears in an internal skip list. The 1,016-test suite in that revision passed while every one of those defects was live. The feature is worth having and will return once it is verified against those shapes rather than against the layouts it was designed for.
55
+
56
+ ### Behavior changes
57
+
58
+ Three observable changes, all in `.env` handling:
59
+
60
+ - A URL value whose userinfo the masking rule cannot rewrite is dropped whole instead of passing through. This covers a password holding a raw `/`, `?`, `#` or space, or one beginning with digits. The v2.5.0 assertion pinning the old pass-through result for `postgres://app:pa/ss@db:5432/app` is superseded, not relaxed. Percent-encode the password (`/` as `%2F`) to keep scheme, host and path visible in the generated docs.
61
+ - `envInfo.host` and `envInfo.apiTarget` are `null` rather than `"***REDACTED***"` when their value was dropped, so the corresponding CLAUDE.md §3 row is omitted instead of showing the sentinel.
62
+ - `init` prints a Phase 1 warning naming the keys it dropped. No value material is printed.
63
+
64
+ ### Known limits
65
+
66
+ - Where a URL's authority is genuinely ambiguous the scheme decides, so an `https://` value whose password both begins with digits and contains a `/` (`https://user:12345/6@host/x`) is read as a path and left masked-through. Ordinary HTTP basic-auth credentials, and every DSN scheme, are caught.
67
+ - Oracle-style JDBC DSNs (`jdbc:oracle:thin:scott/tiger@//dbhost:1521/ORCL`) are still not masked by any rule: the `://` gate cannot match them, the scheme-less branch fires only on the Go/MySQL `@tcp(`/`@unix(` shape, and neither `SPRING_DATASOURCE_URL` nor `JDBC_URL` trips a key-name rule. This shape is common in the legacy-Java trees v2.5.1 opened up and is the largest remaining hole; it is called out here rather than left implicit.
68
+ - Gradle build files are still not comment-stripped (Maven poms are), so a `//`-commented Spring coordinate still counts, as it always has for Boot.
69
+ - Cross-file inheritance (a corporate parent pom that itself extends `spring-boot-starter-parent`; a version held only in a parent pom outside the repository) remains out of scope; the resulting `null` falls through to LLM-side analysis.
70
+
71
+ ### Migration
72
+
73
+ None required, and no scanner output changes: domains, counts and patterns are exactly what v2.5.1 produced. If a project's `.env` holds a URL password containing a raw `/`, `?`, `#` or space, or one beginning with digits, that value is now dropped from `project-analysis.json` and `init` says so in Phase 1. Percent-encode the password and re-run to restore the host.
74
+
31
75
  ## [2.5.1] — 2026-09-08
32
76
 
33
77
  Patch release. Two things: the v2.5.0 credential-masking headline had a hole in it, and `init` on pre-Boot Java projects — the shape of most enterprise/SI codebases — reported "No language detected" and stopped. One new file (`plan-installer/jvm-detect.js`, pure text functions). `project-analysis.json` gains optional, additive fields only (`stack.packaging`, `stack.springFrameworkVersion`, `stack.sourceLayout`); no existing field changes type or meaning, and the `detected` array of every Spring Boot project is byte-identical to v2.5.0 output. Test suite: 825 → **974 / 974** pass (149 added, 0 changed).
@@ -50,7 +94,7 @@ Patch release. Two things: the v2.5.0 credential-masking headline had a hole in
50
94
  - **IDE metadata as evidence.** `.settings/org.eclipse.jdt.core.prefs` `compiler.compliance` (outranks the JRE container name), custom JRE names in `.classpath` (`jdk1.6.0_45`, `jdk-17.0.2`), `.classpath` `kind="lib"` / `kind="var"` jar paths (jars that are *not committed* — `M2_REPO/…/spring-webmvc-3.0.5.RELEASE.jar` still names the version), IntelliJ `.idea/misc.xml` `languageLevel="JDK_1_7"`, NetBeans `nbproject/project.properties` (`javac.source`, `file.reference.*.jar`). Jars under `lib/spring/`, `lib/db/` sub-folders are found (recursive under the lib roots).
51
95
  - **Deployment descriptor and Spring XML.** `WEB-INF/web.xml` naming `org.springframework.web.servlet.DispatcherServlet` / `ContextLoaderListener` is Spring MVC evidence and a `war` signal; `ActionServlet` / `StrutsPrepareAndExecuteFilter` add a `struts` / `struts2` tag; `<web-app version="2.5">` adds `servlet 2.5`. `src/test/**` is excluded (a test-resources `web.xml` is not deployment evidence), and the whole block is skipped for Spring Boot projects — Boot's WAR packaging comes from the build file, and a Boot project's `detected` array stays byte-identical to v2.5.0; Struts / JSF coordinate tags are likewise not added to Boot projects. Spring XML configs' schema locations (`spring-beans-3.0.xsd`) yield a **major.minor** version — the lowest-fidelity source, consulted only when nothing else pinned a version, never overriding one (`spring-xsd 3.0` in `detected`).
52
96
  - **Precedence against Node / Python.** All of the above runs *after* the Node and Python blocks and may only fill a language nobody claimed — or reclaim a *provisional* one: a root `package.json` that exists for gulp / jQuery / Tailwind tooling (no Node framework, no frontend framework detected) beside `build.xml`, a `.project` with `javanature`, a sibling pom/gradle, or a `WEB-INF/web.xml`. A Next.js / Express / Django project with a stray `.idea/misc.xml` or a vendored `tools/lib/*.jar` is never flipped to Java; a jar directory with no `*.java` sources claims nothing (no language, no framework, no DB). The reclaim is *parked*, not committed: if no JVM block then actually claims the project — a `build.xml` that is Phing or an empty stub, no `*.java` anywhere — the Node language is restored unchanged, and a `<project>` root element alone is not Ant (a `<javac>` task or `*.java` sources is required). A reclaimed Java project takes the JVM package manager (`ant` / `maven`), not the asset tooling's `npm`. EUC-KR-encoded poms with Korean comments detect normally (coordinates are ASCII).
53
- - **eGovFrame (전자정부 표준프레임워크).** `egovframework.rte[.*]` coordinates (Maven or Gradle, `${egovframework.rte.version}` resolved) are reported as `spring-framework` — the templates and prompts understand Spring, and eGovFrame is Spring MVC underneath — plus an `egovframe <version>` tag in `detected`. Its `<spring.maven.version>` property is on the Framework-version property list.
97
+ - **eGovFrame (Korea's e-Government Standard Framework).** `egovframework.rte[.*]` coordinates (Maven or Gradle, `${egovframework.rte.version}` resolved) are reported as `spring-framework` — the templates and prompts understand Spring, and eGovFrame is Spring MVC underneath — plus an `egovframe <version>` tag in `detected`. Its `<spring.maven.version>` property is on the Framework-version property list.
54
98
  - **`scan-java` reads legacy source roots.** Every pattern in the scanner is written against `src/main/java` / `src/main/resources`; those two leading segments are now rewritten per discovered root instead of being literal. Roots, in order: every `[<module>/]src/main/java` (unchanged, and if any exists the legacy fallbacks are **not** consulted — a modern tree with a stray top-level `src/` cannot be mis-rooted); otherwise `.classpath` `<classpathentry kind="src" path="…"/>` entries (test folders excluded — including a plain `test/` that `src/test/**` ignore rules would not catch), `build.xml` `<javac srcdir="${src}">` with `<property>` resolution, then `src/java`, `src`, `JavaSource`, `java`, `WebContent/WEB-INF/src` when they hold `*.java` (nested pairs keep the deeper one). For a legacy root the resources root is the java root itself — iBatis-era projects keep sqlmap XML beside the classes. `stack.sourceLayout: "legacy"` is set when a legacy root is in use. An Ant + `WEB-INF/lib` project with `src/com/acme/erp/{controller,service,dao}/` now yields `java 6 · spring-framework 3.0.5.RELEASE · oracle · ibatis · ant · war` and its three Pattern C domains; before, `init` reported "No language detected".
55
99
 
56
100
  ### Changed
package/README.de.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-981%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
package/README.es.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-981%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
package/README.fr.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-981%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
package/README.hi.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-981%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
package/README.ja.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-981%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
package/README.ko.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-981%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-981%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
package/README.ru.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-981%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
package/README.vi.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-981%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/claudeos-core.svg?logo=npm&label=npm)](https://www.npmjs.com/package/claudeos-core)
4
4
  [![CI](https://img.shields.io/github/actions/workflow/status/claudeos-core/claudeos-core/test.yml?branch=master&logo=github&label=CI)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
- [![tests](https://img.shields.io/badge/tests-974%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
5
+ [![tests](https://img.shields.io/badge/tests-981%20passing-brightgreen?logo=node.js&logoColor=white)](https://github.com/claudeos-core/claudeos-core/actions/workflows/test.yml)
6
6
  [![node](https://img.shields.io/node/v/claudeos-core.svg?logo=node.js&logoColor=white&label=node)](https://nodejs.org/)
7
7
  [![license](https://img.shields.io/npm/l/claudeos-core.svg?color=blue)](LICENSE)
8
8
  [![downloads](https://img.shields.io/npm/dm/claudeos-core.svg?logo=npm&color=blue&label=downloads)](https://www.npmjs.com/package/claudeos-core)
package/lib/env-parser.js CHANGED
@@ -301,6 +301,108 @@ function isSensitiveVarName(name) {
301
301
  * Scheme, host, port, path and query are preserved so consumers can still
302
302
  * identify the DB engine / host. Non-URL values pass through unchanged.
303
303
  */
304
+ // The userinfo rule maskUrlCredentials() applies. Shared so the backstop below
305
+ // can ask "did this already get masked?" instead of guessing from the result.
306
+ const USERINFO_RE = /^([a-z][a-z0-9+.:-]*:\/\/)([^/?#\s]*)@([^@/?#\s]+)/i;
307
+ // Schemes whose values routinely carry an `@` inside a PATH: scoped npm
308
+ // packages, image variants, `/users/@me`. A DSN scheme essentially never does.
309
+ const WEB_SCHEME_RE = /^(?:https?|wss?)$/i;
310
+ // A token that can follow the `@` terminating an authority: a host, an IPv6
311
+ // literal in brackets, optionally `:port`, then end-of-value or a delimiter.
312
+ const HOST_AFTER_AT_RE = /^(?:\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9._~%-]+)(?::\d+)?(?:[/?#]|$)/;
313
+ // A complete `host[:port]` with a NUMERIC port.
314
+ const HOST_PORT_RE = /^(?:[A-Za-z0-9._~-]+)(?::\d+)?$/;
315
+
316
+ /**
317
+ * v2.5.2 — True when a `scheme://…` value carries userinfo that
318
+ * maskUrlCredentials() could not rewrite, because the password contains a raw
319
+ * `/`, `?`, `#` or space and so pushed the real authority past the point where
320
+ * the userinfo rule is forced to stop.
321
+ *
322
+ * The earlier implementation guessed from the TRUNCATED authority — the text
323
+ * before the first `/?#` — and asked whether the part after its `:` was
324
+ * all-digits. That inverted the test for the most common leak of all: in
325
+ * `postgres://user:12345/6@db/app` the truncated authority is `user:12345`,
326
+ * whose tail IS all digits, so a password merely BEGINNING with digits (which
327
+ * base64-generated passwords routinely do) was waved through. It also could
328
+ * not see a password holding a space, since the userinfo rule's own character
329
+ * class excludes whitespace.
330
+ *
331
+ * This version instead locates the `@` that actually terminates an authority
332
+ * and decides from there. `user:12345` and `a.com:8080` are syntactically
333
+ * indistinguishable, so where ambiguity is irreducible the scheme breaks the
334
+ * tie: for a DSN the value is treated as credentials, for http/https/ws/wss as
335
+ * a path.
336
+ *
337
+ * postgres://u:p/w@host/db true password holds `/`
338
+ * postgres://user:12345/6@db/app true digit-leading password
339
+ * postgres://u:pa ss@host/db true password holds a space
340
+ * redis://:pw?x@host/0 true password holds `?`
341
+ * postgres://u:p@host/db false the userinfo rule handles it
342
+ * https://cdn.example.com/npm/@x/y false no credential shape at all
343
+ * http://a.com:8080/img/@2x.png false web scheme, complete host:port
344
+ * http://[::1]:8080/img/@2x.png false bracketed IPv6 is a host
345
+ * mongodb://h:port/db?x=a@b false the `@` sits in the query
346
+ * https://api:${PORT}/v1/@me false unexpanded template
347
+ */
348
+ function hasUnmaskedUrlCredentials(value) {
349
+ if (typeof value !== "string") return false;
350
+ const m = value.match(/^([a-z][a-z0-9+.-]*(?::[a-z][a-z0-9+.-]*)*):\/\/(.*)$/i);
351
+ if (!m) return false;
352
+ const rest = m[2];
353
+ if (!rest || !rest.includes("@")) return false;
354
+ // An unexpanded `${VAR}` is a template, not a live secret. parseEnvContent
355
+ // deliberately does not expand these, so redacting one loses a host for
356
+ // nothing.
357
+ if (/\$\{[^}]*\}/.test(rest)) return false;
358
+ // Already masked by the userinfo rule — nothing is hidden.
359
+ if (USERINFO_RE.test(value)) return false;
360
+
361
+ const scheme = m[1].split(":")[0].toLowerCase(); // `jdbc:postgresql` → `jdbc`
362
+ // What a strict parser would read as the authority: everything up to the
363
+ // first `/`, `?` or `#`.
364
+ const head = rest.slice(0, rest.search(/[/?#]/) === -1 ? rest.length : rest.search(/[/?#]/));
365
+ // A bracketed IPv6 literal can only ever be a host, never userinfo.
366
+ if (head.startsWith("[")) return false;
367
+ // No `user:secret` shape at all. The empty-user form (`redis://:pw@host`)
368
+ // starts with `:`, so it is admitted here.
369
+ if (!head.includes(":")) return false;
370
+ // For a web scheme, a head that is ALREADY a complete `host:port` means the
371
+ // authority ended there and the `@` belongs to the path.
372
+ if (WEB_SCHEME_RE.test(scheme) && HOST_PORT_RE.test(head)) return false;
373
+
374
+ // The `@` that would terminate the real authority: the last one followed by
375
+ // something that can be a host.
376
+ // The loop stops at `i > 0`, NOT `i !== -1`. `String.prototype.lastIndexOf`
377
+ // clamps a negative `fromIndex` to 0 rather than returning -1, so an `@`
378
+ // sitting at index 0 that fails the host test re-finds itself forever and
379
+ // hangs `init` at 100% CPU with no error (`postgres://@ :x` reproduces it).
380
+ // Stopping at index 0 loses nothing: an `@` there means empty userinfo.
381
+ for (let i = rest.lastIndexOf("@"); i > 0; i = rest.lastIndexOf("@", i - 1)) {
382
+ // `continue`, NOT `return false`. Abandoning the whole search on the first
383
+ // `@` that turns out to be query content let a DSN carrying an email or a
384
+ // redirect URL in its query string defeat the backstop entirely:
385
+ // `postgres://app:pa/ss@db/app?redirect=user@host` starts at the LAST `@`,
386
+ // decides it is query content, and never examines the earlier `@` that
387
+ // terminates the real authority.
388
+ if (!HOST_AFTER_AT_RE.test(rest.slice(i + 1))) continue;
389
+ // If a `?` precedes this `@` AND a `/` precedes that `?`, then a path had
390
+ // already begun before the query started, so the authority was long since
391
+ // over and this `@` is query content (`mongodb://h:port/db?x=a@b`). When
392
+ // the `?` comes before any `/` it is inside the password instead
393
+ // (`redis://:pw?x@host/0`).
394
+ const q = rest.indexOf("?");
395
+ if (q !== -1 && q < i && rest.slice(0, q).includes("/")) continue;
396
+ return true;
397
+ }
398
+ return false;
399
+ }
400
+
401
+ const REDACTED = "***REDACTED***";
402
+ // Scalar env-derived fields (host, apiTarget) are rendered directly into
403
+ // generated docs, so the sentinel must never become their value.
404
+ const nullIfRedacted = (v) => (v === REDACTED ? null : v);
405
+
304
406
  function maskUrlCredentials(value) {
305
407
  if (typeof value !== "string") return value;
306
408
  // Scheme may itself contain `:` (`jdbc:postgresql://`, `jdbc:mysql://`).
@@ -318,9 +420,36 @@ function maskUrlCredentials(value) {
318
420
  // The parameter NAME is kept, the value becomes `***`.
319
421
  const PARAM_RE = /([?&;](?:password|passwd|pwd|pass|secret|token|access[_-]?key|secret[_-]?key|api[_-]?key|sas|signature)=)[^&;\s]*/gi;
320
422
  if (/^[a-z][a-z0-9+.:-]*:\/\//i.test(value)) {
321
- return value
322
- .replace(/^([a-z][a-z0-9+.:-]*:\/\/)([^/?#\s]*)@([^@/?#\s]+)/i, "$1***:***@$3")
423
+ const userinfoMasked = USERINFO_RE.test(value);
424
+ const masked = value
425
+ .replace(USERINFO_RE, "$1***:***@$3")
323
426
  .replace(PARAM_RE, "$1***");
427
+ // v2.5.2 — last-resort backstop for a password containing a raw `/`, `?`
428
+ // or `#` (`postgres://u:p/w@host/db`). The rule above deliberately does
429
+ // not rewrite an `@` that appears after the first `/` — such an `@`
430
+ // normally belongs to the path (`https://cdn.example.com/npm/@scope/pkg`)
431
+ // and masking it would replace the real host. The value therefore passed
432
+ // through verbatim, and because its key is `DATABASE_URL` the key-name
433
+ // rule did not backstop it either: it was the one combination that could
434
+ // still write a plaintext password into project-analysis.json, which
435
+ // Pass 3/4 prompts instruct the model to read.
436
+ //
437
+ // Base64-generated passwords contain `/` routinely, so this is not an
438
+ // exotic shape. When the value carries the tell (see
439
+ // hasUnmaskedUrlCredentials) the WHOLE value is dropped rather than
440
+ // rewritten — the host cannot be located reliably once the authority is
441
+ // ambiguous, and a lost host is a far cheaper failure than a leaked
442
+ // credential. Callers are told which key it was via
443
+ // envInfo.credentialWarnings.
444
+ //
445
+ // Gated on the USERINFO rule not having fired — NOT on `masked === value`.
446
+ // PARAM_RE may rewrite a query parameter on the same value
447
+ // (`postgres://u:p/w@host/db?sslmode=require&password=x`); a
448
+ // "did anything change" gate would then skip the backstop and let `p/w`
449
+ // through verbatim. Whether the userinfo rule matched is the only signal
450
+ // that says the authority itself was masked.
451
+ if (!userinfoMasked && hasUnmaskedUrlCredentials(value)) return "***REDACTED***";
452
+ return masked;
324
453
  }
325
454
  // Scheme-less credentials are recognized ONLY in the Go/MySQL DSN shape
326
455
  // (`user:pw@tcp(host:3306)/db`, `user:pw@unix(/path)/db`); the password may
@@ -377,9 +506,25 @@ function readStackEnvInfo(root) {
377
506
  return {
378
507
  source: file,
379
508
  vars: redactSensitiveVars(vars),
509
+ // v2.5.2 — the keys whose value `maskUrlCredentials` dropped whole rather
510
+ // than partially masking. The credential is not leaked, but the user loses
511
+ // the host for that key, so `init` names the keys in its Phase 1 summary
512
+ // instead of silently swallowing them. Key NAMES only, never any part of
513
+ // the value.
514
+ //
515
+ // Derived from what `maskUrlCredentials` ACTUALLY returned, so the list can
516
+ // never name a key whose value was kept (masked) rather than dropped. The
517
+ // `!== REDACTED` guard covers an env value that is literally the sentinel.
518
+ credentialWarnings: Object.keys(vars).filter(
519
+ k => !isSensitiveVarName(k) && vars[k] !== REDACTED && maskUrlCredentials(vars[k]) === REDACTED
520
+ ),
380
521
  port: extractPort(vars),
381
- host: maskUrlCredentials(extractHost(vars)),
382
- apiTarget: maskUrlCredentials(extractApiTarget(vars)),
522
+ // A value the backstop dropped whole must not travel on as the literal
523
+ // sentinel: `host` / `apiTarget` are rendered straight into CLAUDE.md §3,
524
+ // and the scaffold's only sentinel guard covers `envInfo.vars`. Null makes
525
+ // the row simply absent, which is what the scaffold already handles.
526
+ host: nullIfRedacted(maskUrlCredentials(extractHost(vars))),
527
+ apiTarget: nullIfRedacted(maskUrlCredentials(extractApiTarget(vars))),
383
528
  };
384
529
  }
385
530
 
@@ -394,6 +539,7 @@ module.exports = {
394
539
  isSensitiveVarName,
395
540
  redactSensitiveVars,
396
541
  maskUrlCredentials,
542
+ hasUnmaskedUrlCredentials,
397
543
  // Exported for test visibility:
398
544
  ENV_FILE_ORDER,
399
545
  PORT_VAR_KEYS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeos-core",
3
- "version": "2.5.1",
3
+ "version": "2.5.2",
4
4
  "description": "Auto-generate Claude Code documentation from your actual source code — Standards, Rules, Skills, and Guides tailored to your project",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
@@ -52,7 +52,23 @@ async function main() {
52
52
  console.log(` Database: ${stack.database || "none"}`);
53
53
  }
54
54
  console.log(` ORM: ${stack.orm || "none"}`);
55
- console.log(` PackageMgr: ${stack.packageManager || "none"}\n`);
55
+ console.log(` PackageMgr: ${stack.packageManager || "none"}`);
56
+ // v2.5.2 — a URL value whose password holds a raw `/`, `?` or `#` cannot
57
+ // have its userinfo rewritten without risking the host, so the whole value
58
+ // is dropped. Say so: silently losing a DATABASE_URL would otherwise look
59
+ // like a detection bug. Key names only, never any part of the value.
60
+ // Both env sources are reported. A sub-directory SPA keeps its own
61
+ // `frontend/.env`, read into `stack.frontendEnvInfo`, and reading only
62
+ // `stack.envInfo` swallowed exactly the drop this warning exists to announce.
63
+ for (const info of [stack.envInfo, stack.frontendEnvInfo]) {
64
+ const credWarn = (info && info.credentialWarnings) || [];
65
+ if (!credWarn.length) continue;
66
+ console.warn(`\n ⚠️ Credential-shaped value dropped from ${credWarn.join(", ")} (${info.source}).`);
67
+ console.warn(" The password contains a raw '/', '?', '#' or space, which makes the URL's host");
68
+ console.warn(" ambiguous, so the value was redacted whole rather than partially masked.");
69
+ console.warn(" Percent-encode the password (e.g. '/' as %2F) to keep the host visible.");
70
+ }
71
+ console.log("");
56
72
 
57
73
  // Phase 2: Structure scan
58
74
  console.log(" [Phase 2] Scanning structure...");