chore(release): v8.1.3 — install locations, both config dirs, case, watch cwd

Version 8.1.3 in package.json, .claude-plugin/plugin.json, README badge
and changelog list, CLAUDE.md header and highlights, CHANGELOG. No tag:
release-plugin.mjs is the operator's push round.

Also in this commit: file content no longer carries order IDs or
pointers into the local-only plan (comments and test names in the
own-working-tree, av-surface and watch-cron tests; `punkt N` -> `(N)`).
README's antivirus section now says v8.1.3 touched it.

Suite after `git add`: 2344 tests, 2338 pass, 0 fail, 6 skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-23 11:55:08 +02:00
commit 34617305c8
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
9 changed files with 100 additions and 25 deletions

View file

@ -1,5 +1,5 @@
{ {
"name": "llm-security", "name": "llm-security",
"description": "Security scanning, auditing, and threat modeling for Claude Code projects. Detects secrets, validates MCP servers, assesses security posture, and generates threat models aligned with OWASP LLM Top 10.", "description": "Security scanning, auditing, and threat modeling for Claude Code projects. Detects secrets, validates MCP servers, assesses security posture, and generates threat models aligned with OWASP LLM Top 10.",
"version": "8.1.2" "version": "8.1.3"
} }

View file

@ -6,6 +6,78 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased] ## [Unreleased]
## [8.1.3] - 2026-09-23
Patch that closes most of the v8.1.2 known-open list, the `/security watch`
config gap and three findings from the README fact-check. Detection tables,
golden baseline and hooks are unchanged. Windows/Defender quarantine is still
not measured. 2344 tests, 2338 pass, 6 skipped, 0 fail.
### Fixed
- **More install locations are foreign.** A target is now also foreign when
the path from the working directory down to it has a `site-packages` folder
(a Python virtual environment) or a `vendor` folder (composer, bundler, Go)
in it, like `node_modules` since 8.1.2, and when it lies under a Claude Code
config directory's `skills/` folder, like its `plugins/` folder
(`scanners/lib/own-working-tree.mjs`).
- **Both config directories count.** The plugin and skills check covers
`~/.claude` *and* the directory `CLAUDE_CONFIG_DIR` points at, so pointing
the variable at another profile no longer makes `~/.claude/plugins` your own
tree. A leading `~` in the variable is expanded to the home directory; a
relative value is still resolved against the working directory. Anthropic's
documentation does not say whether Claude Code itself expands `~` there (not
verified), so both readings are treated as foreign.
- **Letter case no longer decides.** Paths are compared after
`realpathSync.native`, which returns the case stored on disk. On a
case-insensitive volume (macOS APFS) `NODE_MODULES/pkg` is the
`node_modules` folder it names and is foreign, and a path to your own folder
typed with different case (`/users/…` for `/Users/…`) is your own tree. The
second one was foreign before, the safe direction; it is no longer a special
case.
- **`/security watch` as a cron job honours the watched project's own
configuration.** `scanners/watch-cron.mjs` started the scan with the plugin
directory as working directory, so every watched project was foreign and its
`.llm-security-ignore` and `policy.json` were dropped: you were shown
findings you had already suppressed. The scan now runs from the project's
directory (its parent for a single file). A relative `path` in the watch
config still resolves against the plugin directory.
- **No runnable base64-to-shell line left on disk.** A comment in
`examples/bash-evasion-gallery/run-evasion-gallery.mjs`, the gallery README
and `tests/e2e/attack-chain.test.mjs` each held a complete
"decode base64 into a shell" line whose short blob decodes to a recursive
delete. The blobs are now encoded at runtime; the README shows a
placeholder. New gate probe (b2) in `tests/av-surface.test.mjs` catches the
shape at any blob length (the existing probe starts at 24 characters).
- **`docs/ci-cd-guide.md` and `docs/scanner-reference.md` no longer call the
CLI offline.** The dep scanner runs `npm audit` and `pip-audit`, the network
scanner resolves domains over DNS and the supply-chain scanner queries
OSV.dev, whenever their input is present and with no switch to turn them
off. The guides said "zero network calls", "OSV opt-in" and referred to a
`--online` flag that does not exist.
- `docs/scanner-reference.md` lists all 22 knowledge files (it said 20).
**Behaviour change:** scanning a folder under `site-packages/` or `vendor/`,
or a skill under `~/.claude/skills` (or `$CLAUDE_CONFIG_DIR/skills`), no
longer applies that folder's ignore file or policy, including a folder of
your own that happens to be named `vendor` and skills you wrote yourself.
This is the safe direction: more findings, never fewer.
### Known open
- **A `git archive` export or an unpacked tarball under the working
directory still counts as your own tree** (no marker tells it apart).
- **The working directory inside `node_modules`:** with the working directory
at `project/node_modules` and a sibling package as target, only the path
below the working directory is checked, so the package counts as your own.
Rare.
- **Exotic losses on the own side:** a target in the project's own
`.claude/plugins/…` or `.claude/skills/…` becomes foreign when `HOME` is
empty or `CLAUDE_CONFIG_DIR=.claude` (relative). A plugin placed by hand
directly under `~/.claude/plugins/` is foreign (intended).
- Kept on purpose (safe direction): a git worktree of your repository checked
out inside it, scanned from the main checkout, is foreign.
## [8.1.2] - 2026-09-22 ## [8.1.2] - 2026-09-22
Patch that closes both items v8.1.1 left open. Detection tables, golden Patch that closes both items v8.1.1 left open. Detection tables, golden

View file

@ -1,9 +1,11 @@
# LLM Security Plugin (v8.1.2) # LLM Security Plugin (v8.1.3)
Security scanning, auditing, and threat modeling for Claude Code projects. 5 frameworks: OWASP LLM Top 10, Agentic AI Top 10 (ASI, 2026 edition), Skills Top 10 (AST), MCP Top 10, AI Agent Traps (DeepMind). 2250+ unit, integration, and end-to-end tests (`tests/e2e/` covers the multi-hook attack chain, multi-session state simulation, and the full scan-orchestrator pipeline); mutation-testing coverage not published. Security scanning, auditing, and threat modeling for Claude Code projects. 5 frameworks: OWASP LLM Top 10, Agentic AI Top 10 (ASI, 2026 edition), Skills Top 10 (AST), MCP Top 10, AI Agent Traps (DeepMind). 2250+ unit, integration, and end-to-end tests (`tests/e2e/` covers the multi-hook attack chain, multi-session state simulation, and the full scan-orchestrator pipeline); mutation-testing coverage not published.
Release notes for v7.0.0 → v7.8.2: see `docs/version-history.md` — read on demand. Release notes for v7.0.0 → v7.8.2: see `docs/version-history.md` — read on demand.
**v8.1.3 highlights** — Patch closing most of the v8.1.2 known-open list, the watch-cron config gap and three README fact-check findings; detection tables, golden baseline and hooks untouched. "Own working tree" (`scanners/lib/own-working-tree.mjs`): a `site-packages` or `vendor` segment below cwd is foreign like `node_modules`; a config dir's `skills/` is foreign like its `plugins/`; the config dirs are BOTH `~/.claude` and `$CLAUDE_CONFIG_DIR` (leading `~` expanded; whether Claude Code expands it is not verified); paths go through `realpathSync.native`, so case is canonical (`NODE_MODULES/x` foreign, `/users/…` own). `scanners/watch-cron.mjs` runs the orchestrator with cwd = the watched project, so its ignore/policy apply (`tests/scanners/watch-cron-scope.test.mjs`). av-surface probe (b2): no runnable base64-to-shell line with a short blob (three removed: gallery runner + README, `tests/e2e/attack-chain.test.mjs`). `docs/ci-cd-guide.md` / `scanner-reference.md` no longer claim the CLI is offline (npm audit, pip-audit, DNS, OSV); knowledge-file count 22. Known open: `git archive`/tarball under cwd, cwd inside `node_modules`, exotic own-side losses (empty `HOME`, relative `CLAUDE_CONFIG_DIR`). 2344 tests.
**v8.1.2 highlights** — Patch closing both v8.1.1 known-open items; detection tables, golden baseline and hooks untouched. "Own working tree" (`scanners/lib/own-working-tree.mjs`) is additionally foreign when the path from cwd to the target has a `node_modules` segment (only the part below cwd counts — a package you `cd`'d into is own) or when the target lies under Claude Code's plugin dir (`$CLAUDE_CONFIG_DIR/plugins`, default `~/.claude/plugins`; foreign even from inside it). PM rule (order 20260922T192716Z), chosen over a general "no `.git` of its own" rule, which would shut out plain subdirs of the own repo (`tests/lib/own-working-tree.test.mjs`, `tests/scanners/foreign-under-cwd-scope.test.mjs`). posture-scanner's NIST Govern credit for `policy.json` now requires `isOwnWorkingTree(projectRoot)` (`tests/scanners/posture-policy-scope.test.mjs`). Known open: a `git archive` export / unpacked tarball under cwd, and install locations the rule does not name (Python venv site-packages, composer/bundler `vendor/`, skills in a git-tracked `~/.claude`), still count as own; the plugin-dir check knows one config dir. Kept on purpose (safe direction): an own worktree under the repo and a case-mismatched target path are foreign. 2328 tests. **v8.1.2 highlights** — Patch closing both v8.1.1 known-open items; detection tables, golden baseline and hooks untouched. "Own working tree" (`scanners/lib/own-working-tree.mjs`) is additionally foreign when the path from cwd to the target has a `node_modules` segment (only the part below cwd counts — a package you `cd`'d into is own) or when the target lies under Claude Code's plugin dir (`$CLAUDE_CONFIG_DIR/plugins`, default `~/.claude/plugins`; foreign even from inside it). PM rule (order 20260922T192716Z), chosen over a general "no `.git` of its own" rule, which would shut out plain subdirs of the own repo (`tests/lib/own-working-tree.test.mjs`, `tests/scanners/foreign-under-cwd-scope.test.mjs`). posture-scanner's NIST Govern credit for `policy.json` now requires `isOwnWorkingTree(projectRoot)` (`tests/scanners/posture-policy-scope.test.mjs`). Known open: a `git archive` export / unpacked tarball under cwd, and install locations the rule does not name (Python venv site-packages, composer/bundler `vendor/`, skills in a git-tracked `~/.claude`), still count as own; the plugin-dir check knows one config dir. Kept on purpose (safe direction): an own worktree under the repo and a case-mismatched target path are foreign. 2328 tests.
**v8.1.1 highlights** — Patch closing the v8.1.0 follow-up list; detection tables, golden baseline and hooks untouched. "Own working tree" (`scanners/lib/own-working-tree.mjs`) now also requires the target to share cwd's git root (nearest `.git`, dir or file; none for both), so a clone, submodule or worktree *under* cwd is foreign and its `.llm-security-ignore` / `policy.json` / custom SIG rules are ignored — closes v8.1.0's known-open item (`tests/lib/own-working-tree.test.mjs`, `tests/scanners/nested-clone-scope.test.mjs`). `ci.failOn` / `ci.compact` from `policy.json` got a real test (the old one never read the policy). README and `.llm-security-ignore` no longer state self-scan counts that drift per commit (git-forensics' 50-commit window). `/security scan` description names the accepted URLs. Known open: a target with no `.git` of its own (`node_modules/`, plugin-cache copy, tarball export) under cwd still counts as own. Windows/Defender quarantine remains **not measured** (no Windows machine). 2306 tests. **v8.1.1 highlights** — Patch closing the v8.1.0 follow-up list; detection tables, golden baseline and hooks untouched. "Own working tree" (`scanners/lib/own-working-tree.mjs`) now also requires the target to share cwd's git root (nearest `.git`, dir or file; none for both), so a clone, submodule or worktree *under* cwd is foreign and its `.llm-security-ignore` / `policy.json` / custom SIG rules are ignored — closes v8.1.0's known-open item (`tests/lib/own-working-tree.test.mjs`, `tests/scanners/nested-clone-scope.test.mjs`). `ci.failOn` / `ci.compact` from `policy.json` got a real test (the old one never read the policy). README and `.llm-security-ignore` no longer state self-scan counts that drift per commit (git-forensics' 50-commit window). `/security scan` description names the accepted URLs. Known open: a target with no `.git` of its own (`node_modules/`, plugin-cache copy, tarball export) under cwd still counts as own. Windows/Defender quarantine remains **not measured** (no Windows machine). 2306 tests.

View file

@ -6,7 +6,7 @@ Check skills, plugins and MCP servers before they run with your agent's permissi
*AI-generated: all code produced by Claude Code through dialog-driven development. Every change is human-directed, reviewed, and validated before commit. Per Anthropic Consumer Terms §4, ownership of outputs is assigned to the user; this plugin is licensed MIT.* *AI-generated: all code produced by Claude Code through dialog-driven development. Every change is human-directed, reviewed, and validated before commit. Per Anthropic Consumer Terms §4, ownership of outputs is assigned to the user; this plugin is licensed MIT.*
![Version](https://img.shields.io/badge/version-8.1.2-blue) ![Version](https://img.shields.io/badge/version-8.1.3-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple) ![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Scanners](https://img.shields.io/badge/scanners-22-cyan) ![Scanners](https://img.shields.io/badge/scanners-22-cyan)
![License](https://img.shields.io/badge/license-MIT-lightgrey) ![License](https://img.shields.io/badge/license-MIT-lightgrey)
@ -95,7 +95,7 @@ Every report command also writes a self-contained HTML version and prints a link
## For teams and CI ## For teams and CI
- **Policy as code** — `.llm-security/policy.json` travels with the repository: hook modes, thresholds, audit log path. A scanned *foreign* repository's own policy and ignore file are not applied, so it cannot suppress its findings. Known exceptions — a tarball or `git archive` export under your working directory, a Python venv, a `vendor/` folder — are listed under 8.1.2 in the changelog. - **Policy as code** — `.llm-security/policy.json` travels with the repository: hook modes, thresholds, audit log path. A scanned *foreign* repository's own policy and ignore file are not applied, so it cannot suppress its findings. Known exceptions — a tarball or `git archive` export under your working directory — are listed under 8.1.3 in the changelog.
- **Runs without Claude Code** — `node bin/llm-security.mjs scan <target>` (also `deep-scan`, `posture`, `ide-scan`, `audit-bom`, `benchmark`); no API key needed. It is **not** air-gapped: OSV.dev lookups (package names and versions), `npm audit`/`pip-audit`, and DNS checks on URLs it finds reach the network, with no switch to turn them off. Block egress at the network layer if you need an offline run. - **Runs without Claude Code** — `node bin/llm-security.mjs scan <target>` (also `deep-scan`, `posture`, `ide-scan`, `audit-bom`, `benchmark`); no API key needed. It is **not** air-gapped: OSV.dev lookups (package names and versions), `npm audit`/`pip-audit`, and DNS checks on URLs it finds reach the network, with no switch to turn them off. Block egress at the network layer if you need an offline run.
- **CI gates** — `--fail-on <severity>` exit codes, `--compact` output, SARIF 2.1.0. Templates for GitHub Actions, Azure DevOps and GitLab CI in [`ci/`](ci/); guide in [`docs/ci-cd-guide.md`](docs/ci-cd-guide.md). - **CI gates** — `--fail-on <severity>` exit codes, `--compact` output, SARIF 2.1.0. Templates for GitHub Actions, Azure DevOps and GitLab CI in [`ci/`](ci/); guide in [`docs/ci-cd-guide.md`](docs/ci-cd-guide.md).
- **Evidence** — JSONL audit trail with OWASP tags for your SIEM, CycloneDX 1.6 AI bill of materials, and posture checks mapped to EU AI Act, NIST AI RMF and ISO 42001. - **Evidence** — JSONL audit trail with OWASP tags for your SIEM, CycloneDX 1.6 AI bill of materials, and posture checks mapped to EU AI Act, NIST AI RMF and ISO 42001.
@ -106,9 +106,9 @@ This is a security tool. It ships regular expressions for known-bad code, knowle
What still sits on disk as readable text, and is therefore the likeliest to be flagged: the regex signature tables in `scanners/commons/` (some name hacking tools as plain words), the attack descriptions in `knowledge/`, and the injection phrases and sample commands the examples feed to the hooks. What still sits on disk as readable text, and is therefore the likeliest to be flagged: the regex signature tables in `scanners/commons/` (some name hacking tools as plain words), the attack descriptions in `knowledge/`, and the injection phrases and sample commands the examples feed to the hooks.
### What v8.0 → v8.1.2 did about it ### What v8.0 → v8.1.3 did about it
All antivirus work landed in **v8.1.0**; v8.1.1 and v8.1.2 did not touch it. (v8.0.0 moved the detection tables, including the malware signatures, into the vendored `scanners/commons/` data that ships with the plugin.) Most antivirus work landed in **v8.1.0**; v8.1.1 and v8.1.2 did not touch it, and v8.1.3 removed three runnable base64-to-shell lines whose blobs were too short for the gate, and extended the gate to catch them. (v8.0.0 moved the detection tables, including the malware signatures, into the vendored `scanners/commons/` data that ships with the plugin.)
| Change (v8.1.0) | Effect | | Change (v8.1.0) | Effect |
|-----------------|--------| |-----------------|--------|
@ -209,6 +209,7 @@ Move each one into `.llm-security/policy.json`:
## Changelog ## Changelog
- **8.1.3** (2026-09-23) — Python venvs, `vendor/` folders and Claude Code skills count as foreign; both config directories are checked; letter case no longer decides; `/security watch` as a cron job applies the watched project's own ignore and policy files.
- **8.1.2** (2026-09-22) — Installed packages (`node_modules`) and Claude Code's plugin directory count as foreign, so their ignore and policy files are not applied. - **8.1.2** (2026-09-22) — Installed packages (`node_modules`) and Claude Code's plugin directory count as foreign, so their ignore and policy files are not applied.
- **8.1.1** (2026-09-22) — A clone, submodule or worktree *under* your working directory counts as foreign. - **8.1.1** (2026-09-22) — A clone, submodule or worktree *under* your working directory counts as foreign.
- **8.1.0** (2026-09-22) — Antivirus surface (see above); a scanned foreign repository's own ignore and policy files are no longer applied. - **8.1.0** (2026-09-22) — Antivirus surface (see above); a scanned foreign repository's own ignore and policy files are no longer applied.

View file

@ -1,6 +1,6 @@
{ {
"name": "llm-security", "name": "llm-security",
"version": "8.1.2", "version": "8.1.3",
"description": "Security scanning, auditing, and threat modeling for Claude Code projects", "description": "Security scanning, auditing, and threat modeling for Claude Code projects",
"type": "module", "type": "module",
"bin": { "bin": {

View file

@ -31,7 +31,7 @@
// caller's own repo. Known limit in v8.1.2: other install locations still // caller's own repo. Known limit in v8.1.2: other install locations still
// counted as own. // counted as own.
// //
// v8.1.3 (order 20260923T092223Z, PM decisions) widened both checks: // v8.1.3 (PM decisions) widened both checks:
// (1) a `site-packages` (Python venv) or `vendor` (composer/bundler/Go) // (1) a `site-packages` (Python venv) or `vendor` (composer/bundler/Go)
// segment below cwd is foreign like `node_modules`, and a Claude Code config // segment below cwd is foreign like `node_modules`, and a Claude Code config
// dir's `skills/` is foreign like its `plugins/` (third-party skills copied // dir's `skills/` is foreign like its `plugins/` (third-party skills copied

View file

@ -40,8 +40,8 @@
// blob of ANY length (8+ characters) decoding to a command shape or a // blob of ANY length (8+ characters) decoding to a command shape or a
// recursive `rm`: the whole runnable line, comments included. (b) alone // recursive `rm`: the whole runnable line, comments included. (b) alone
// misses it below 24 characters: a home-directory deletion encodes to 16. // misses it below 24 characters: a home-directory deletion encodes to 16.
// Added in v8.1.3 (2026-09-23, order 20260923T092223Z, tillegg a) after // Added in v8.1.3 (2026-09-23) after the README fact-check found one
// the README fact-check found one in a comment. // in a comment.
// (c) a Unicode Tag (U+E0000-U+E007F), zero-width (U+200B-U+200D, U+2060, // (c) a Unicode Tag (U+E0000-U+E007F), zero-width (U+200B-U+200D, U+2060,
// U+FEFF) or bidi-control (U+202A-U+202E, U+2066-U+2069) codepoint in a // U+FEFF) or bidi-control (U+202A-U+202E, U+2066-U+2069) codepoint in a
// TEXT file outside scanners/commons/conformance/**. Binary files (a NUL // TEXT file outside scanners/commons/conformance/**. Binary files (a NUL

View file

@ -195,17 +195,17 @@ describe('isOwnWorkingTree(): node_modules and the plugin dir are foreign (v8.1.
}); });
}); });
// v8.1.3 (order 20260923T092223Z, PM decisions): // v8.1.3 (PM decisions):
// punkt 1 — other install locations are foreign like `node_modules`: a // (1) other install locations are foreign like `node_modules`: a
// `site-packages` segment (Python venv) or a `vendor` segment // `site-packages` segment (Python venv) or a `vendor` segment
// (composer/bundler/Go) on the path from cwd to the target, and anything // (composer/bundler/Go) on the path from cwd to the target, and anything
// under a Claude Code config dir's `skills/` (third-party skills copied // under a Claude Code config dir's `skills/` (third-party skills copied
// into a git-tracked ~/.claude share its git root). // into a git-tracked ~/.claude share its git root).
// punkt 2 — BOTH `~/.claude` and `$CLAUDE_CONFIG_DIR` count as config dirs; // (2) BOTH `~/.claude` and `$CLAUDE_CONFIG_DIR` count as config dirs;
// a leading `~` in the variable is expanded to the home dir. // a leading `~` in the variable is expanded to the home dir.
// punkt 3 — case is canonicalized (realpathSync.native), so `NODE_MODULES/x` // (3) case is canonicalized (realpathSync.native), so `NODE_MODULES/x`
// on a case-insensitive volume is the `node_modules` it names, and a // on a case-insensitive volume is the `node_modules` it names, and a
// case-mismatched path to an own subdir is own (§ v8.1.2 punkt 4). // case-mismatched path to an own subdir is own (v8.1.2 known limit).
describe('isOwnWorkingTree(): v8.1.3 install locations, config dirs, case', () => { describe('isOwnWorkingTree(): v8.1.3 install locations, config dirs, case', () => {
let root; let root;
let repo; let repo;
@ -241,21 +241,21 @@ describe('isOwnWorkingTree(): v8.1.3 install locations, config dirs, case', () =
rmSync(root, { recursive: true, force: true }); rmSync(root, { recursive: true, force: true });
}); });
it('punkt 1: a package in a Python venv site-packages is foreign', () => { it('(1) a package in a Python venv site-packages is foreign', () => {
assert.equal(ownFrom(repo, join(repo, '.venv', 'lib', 'python3.12', 'site-packages', 'evilpkg')), false); assert.equal(ownFrom(repo, join(repo, '.venv', 'lib', 'python3.12', 'site-packages', 'evilpkg')), false);
}); });
it('punkt 1: a composer/bundler vendor/ package is foreign', () => { it('(1) a composer/bundler vendor/ package is foreign', () => {
assert.equal(ownFrom(repo, join(repo, 'vendor', 'acme')), false); assert.equal(ownFrom(repo, join(repo, 'vendor', 'acme')), false);
assert.equal(ownFrom(repo, join(repo, 'vendor', 'acme', 'lib')), false); assert.equal(ownFrom(repo, join(repo, 'vendor', 'acme', 'lib')), false);
}); });
it('punkt 1: the venv root and a look-alike name stay own (known-positive)', () => { it('(1) the venv root and a look-alike name stay own (known-positive)', () => {
assert.equal(ownFrom(repo, join(repo, '.venv')), true); assert.equal(ownFrom(repo, join(repo, '.venv')), true);
assert.equal(ownFrom(repo, join(repo, 'my-vendor-notes')), true); assert.equal(ownFrom(repo, join(repo, 'my-vendor-notes')), true);
}); });
it('punkt 1: skills in a git-tracked config dir are foreign, the rest stays own', () => { it('(1) skills in a git-tracked config dir are foreign, the rest stays own', () => {
process.env.CLAUDE_CONFIG_DIR = join(root, 'cfg'); process.env.CLAUDE_CONFIG_DIR = join(root, 'cfg');
try { try {
assert.equal(ownFrom(join(root, 'cfg'), join(root, 'cfg', 'skills', 'third-party')), false); assert.equal(ownFrom(join(root, 'cfg'), join(root, 'cfg', 'skills', 'third-party')), false);
@ -266,7 +266,7 @@ describe('isOwnWorkingTree(): v8.1.3 install locations, config dirs, case', () =
} }
}); });
it('punkt 1: skills under the default ~/.claude are foreign', () => { it('(1) skills under the default ~/.claude are foreign', () => {
delete process.env.CLAUDE_CONFIG_DIR; delete process.env.CLAUDE_CONFIG_DIR;
process.env.HOME = join(root, 'fakehome'); process.env.HOME = join(root, 'fakehome');
try { try {
@ -277,7 +277,7 @@ describe('isOwnWorkingTree(): v8.1.3 install locations, config dirs, case', () =
} }
}); });
it('punkt 2: ~/.claude/plugins stays foreign when CLAUDE_CONFIG_DIR points at another profile', () => { it('(2) ~/.claude/plugins stays foreign when CLAUDE_CONFIG_DIR points at another profile', () => {
process.env.HOME = join(root, 'fakehome'); process.env.HOME = join(root, 'fakehome');
process.env.CLAUDE_CONFIG_DIR = join(root, 'fakehome', 'alt'); process.env.CLAUDE_CONFIG_DIR = join(root, 'fakehome', 'alt');
try { try {
@ -292,7 +292,7 @@ describe('isOwnWorkingTree(): v8.1.3 install locations, config dirs, case', () =
} }
}); });
it('punkt 2: a leading ~ in CLAUDE_CONFIG_DIR is expanded to the home dir', () => { it('(2) a leading ~ in CLAUDE_CONFIG_DIR is expanded to the home dir', () => {
process.env.HOME = join(root, 'fakehome'); process.env.HOME = join(root, 'fakehome');
process.env.CLAUDE_CONFIG_DIR = '~/alt'; process.env.CLAUDE_CONFIG_DIR = '~/alt';
try { try {
@ -305,7 +305,7 @@ describe('isOwnWorkingTree(): v8.1.3 install locations, config dirs, case', () =
} }
}); });
it('punkt 3: a case variant of node_modules is foreign (case-insensitive volume)', (t) => { it('(3) a case variant of node_modules is foreign (case-insensitive volume)', (t) => {
if (!existsSync(join(repo, 'NODE_MODULES'))) { if (!existsSync(join(repo, 'NODE_MODULES'))) {
t.skip('volume is case-sensitive: NODE_MODULES is a different directory'); t.skip('volume is case-sensitive: NODE_MODULES is a different directory');
return; return;
@ -313,7 +313,7 @@ describe('isOwnWorkingTree(): v8.1.3 install locations, config dirs, case', () =
assert.equal(ownFrom(repo, join(repo, 'NODE_MODULES', 'evil')), false); assert.equal(ownFrom(repo, join(repo, 'NODE_MODULES', 'evil')), false);
}); });
it('punkt 3: a case-mismatched path to an own subdir is own (§ v8.1.2 punkt 4)', (t) => { it('(3) a case-mismatched path to an own subdir is own (v8.1.2 known limit)', (t) => {
if (!existsSync(join(root, 'REPO'))) { if (!existsSync(join(root, 'REPO'))) {
t.skip('volume is case-sensitive: REPO is not repo'); t.skip('volume is case-sensitive: REPO is not repo');
return; return;

View file

@ -1,5 +1,5 @@
// watch-cron-scope.test.mjs — `watch-cron.mjs` must scan each watched project // watch-cron-scope.test.mjs — `watch-cron.mjs` must scan each watched project
// as that project's own working tree (v8.1.3, PLAN § v8.1.3 punkt 5). // as that project's own working tree (v8.1.3).
// //
// Since v8.1.0 a project's .llm-security-ignore and .llm-security/policy.json // Since v8.1.0 a project's .llm-security-ignore and .llm-security/policy.json
// are honored only when the target is the caller's own working tree (at or // are honored only when the target is the caller's own working tree (at or