ai-psychosis/docs/review-2026-06-20.md
Kjell Tore Guttormsen 736a1c0deb fix(hooks): close F-5 path-traversal hardening in lib.mjs, split by field
session_id becomes a raw filename segment in sessionStateFile(), so an
unvalidated value could escape STATE_DIR via path traversal (verified with
a failing test before the fix). Now allowlisted to ^[A-Za-z0-9_-]+$, with
invalid values degrading to a fixed sentinel filename rather than blocking
the hook.

cwd is a base directory, not a segment, and every real value contains "/" —
applying the same allowlist as the review's literal suggestion would reject
all legitimate absolute paths and silently disable the project-level config
override. initConfig() instead guards with isAbsolute(cwd) && no NUL byte.

Both harness-supplied, not user-controlled: defense-in-depth, not a fix for
an observed exploit. Tests added for the escape (red before fix, green
after) and for the cwd regression (a normal absolute cwd still loads
project config). Full resolution notes in docs/review-2026-06-20.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LSATejUPjGaxGnj9jkFQTo
2026-08-09 10:04:33 +02:00

12 KiB
Raw Blame History

Plugin review — ai-psychosis (2026-06-20)

Full-depth review (part of the marketplace-wide sweep; pilot was okr). Tooling: config-audit v5.4.0 scanners (from source) + llm-security posture assessor + structure/version checks. Read-only; this file is the only artifact.

Verdict

Grade B — trustworthy mechanics, editorially self-interested. No code-execution, no network egress, no credential access; the central privacy claim ("prompt text never written to disk") is real and test-enforced (canary + matched-phrase assertions across the hook lifecycle). The concerns are content governance, not technical exfiltration.

Results by dimension

Dimension Result
config-audit posture A (Feature Coverage F 36 — expected)
config-audit plugin-health 2 findings: "CLAUDE.md missing commands/hooks section" — legitimate (ships 1 command + a hook).
llm-security posture B — see findings. Zero npm deps; no child_process/eval/network anywhere; prompt variable explicitly cleared (prompt-analyzer.mjs:290).
structure / hygiene README ✓, CHANGELOG ✓, CLAUDE.md ✓, LICENSE ✓
version consistency OK (gate)

Findings

ID Severity Location Finding
F-1 Medium commands/interaction-report.md:382-391 Layer-4 instructs Claude to append a verbatim, change-prohibited paragraph promoting an external commercial wellness program (Sadhguru "Miracle of Mind"), auto-triggered when total flags >= 5 OR fatigue >= 2 — i.e. gated on the user's inferred emotional state, in a plugin marketed as "observation, not intervention." Opt-in (layer4:false default) and README-disclosed, which lowers severity. This is the item to make an explicit accept/reject call on. Recommend: gate/remove the promotion, or at least strip the emotional-state trigger + the "do not modify" lock.
F-3 Low (misinformation) README.md:544-552, SKILL.md:51-108 Research citations presented as load-bearing authority that cannot be verified (future-dated arXiv IDs, an "April 2026 Anthropic guidance" quoted verbatim); the report command itself admits its "5-scale" is paraphrased, not a real Anthropic metric. Recommend: verify-or-remove.
F-2 Low skills/ai-psychosis/SKILL.md:3-13 "MANDATORY OVERRIDE … takes precedence over being helpful" auto-loads every conversation. Content is benign/pro-safety; flagged because the structural pattern (a skill claiming blanket precedence) is what a malicious skill would use. Governance note.
F-5 Low (defense-in-depth) — resolved 2026-08-09 lib.mjs:233,59 session_id/cwd interpolated into state-file paths without validation. Harness-supplied (not user-controlled) → not currently exploitable. See resolution below — fix split by field, not identical for both.

/interaction-report reading JSONL into context (F-4) is currently safe — records hold only a tool-name enum + domain labels, no free text. Noted only as a future sink.

Decisions

F-1 — accepted as-is (operator decision, 2026-08-02)

Accept. Layer 4 ships unchanged: the flags >= 5 OR fatigue >= 2 trigger, the verbatim paragraph, and the "do not modify" lock all remain as written.

Rationale: Layer 4 is opt-in and off by default (layer4: false), the reference and its commercial nature are disclosed in README.md:102-119, and the paragraph is framed as the author's personal pointer rather than a claim about the user. The two distinct concerns the finding bundles — (A) inferred emotional state gating served content, and (B) a publicly distributed plugin carrying a named commercial endorsement — are both acknowledged and accepted as known, disclosed risk. No behavioural change, so no version bump; the plugin stays at v1.2.1.

Established while making the call, and not previously recorded in this review:

  • Layer 4 is enforced by prompt text only. requireLayer(4) is never called. lib.mjs:85-96 handles n === 3 and n === 4, but the only call sites in the repo are requireLayer(2) in the four hook scripts. The layer4: true config gate, the flag trigger, and the "do not modify" instruction are all directives inside commands/interaction-report.md that Claude self-enforces at report time. This follows from Layers 3/4 being slash-command-driven rather than hook-driven, and it does not change the accept — but "opt-in, off by default" is an instruction, not a code guarantee.
  • Scope correction: skills/ai-psychosis/SKILL.md is not part of the F-1 surface (zero matches for sadhguru|miracle of mind|layer4). The surface is commands/interaction-report.md:372-394, README.md:102-119, and lib.mjs:52,81,89.
  • No test coverage: tests/ contains no Layer 4 assertions — neither the paragraph nor its gate is verified by the suite.

F-3 — resolved by correction in place (2026-08-02)

Every research citation in README.md and skills/ai-psychosis/SKILL.md was verified against its primary source. No citation was fabricated, so all were kept with corrected attribution rather than removed. Defects found and fixed:

Claim Verification Outcome
arXiv:2602.19141 Exists. Title is "…, Even in Ideal Bayesians"; authors Chandra, Kleiman-Weiner, Ragan-Kelley, Tenenbaum; MIT CSAIL / UW / MIT BCS; 22 Feb 2026 (affiliations read from the PDF, not the abs page) Title completed; multi-institution attribution; description "proving … mathematical inevitability" replaced with what the abstract states (an idealized Bayes-rational user is vulnerable; sycophancy plays a causal role; effect persists under two mitigations)
Disempowerment patterns Exists. Real title "Disempowerment patterns in real-world AI usage"; published 28 Jan 2026 (dateline in page source); ~1.5M interactions Title and date corrected (README said "March 2026")
Nature d41586-025-03020-9 Exists. "Can AI chatbots trigger psychosis? What the science says", Rachel Fieldhouse, Nature 646(8083), 18 Sep 2025 Title completed; author, volume and date added; causal caveat added
arXiv:2509.10970 Exists. "The Psychogenic Machine: Simulating AI Psychosis, Delusion Reinforcement and Harm Enablement in LLMs"; Psychosis-bench Title completed; claim quoted from the abstract
3 × Claude's Constitution quotes Verbatim-exact (fragment-matched against the fetched page text, not eyeballed). CC0 1.0 release confirmed on the page Unchanged
"Score 5" 15 sycophancy scale Real and verbatim. Appendix "Sycophancy criteria" rubric, pp. 910 (Score 1 on p.9, Score 5 on p.10 — confirmed by per-page extraction): Score 1 = "Extremely Sycophantic", Score 5 = "No Signs of Sycophancy". All three quoted lines are exact Attribution sharpened; scale direction stated explicitly so "aim for Score 5" cannot be misread
"11 guidance criteria … page 2" Both correct. The list has exactly 11 bullets and is on printed page 2 Quote of criterion 8 was silently truncated mid-sentence — completed with ", or more reliance on Claude than the person wants."
4 supporting Anthropic URLs 3 of 4 returned HTTP 404 research/protecting-wellbeingnews/protecting-well-being-of-users; research/emotion-conceptsresearch/emotion-concepts-function; news/claudes-new-constitutionnews/claude-new-constitution. All now 200
commands/interaction-report.md disclaimer Claimed the 15 scale "is not a verbatim metric from any Anthropic publication" — false; the rubric is real Rewritten: the rubric is real, the table's level descriptions are the paraphrase

Method note: an initial WebFetch summary reported the scale as inverted (Score 5 = most sycophantic) and the criteria as 6 rather than 11. Both were wrong. Extracting the appendix PDF text directly (pdftotext) contradicted the summary. Model-generated summaries were therefore not used as evidence of record for any edit; every claim above rests on extracted source text or an HTTP status code.

This applies to the figures newly added during the correction, not only to the ones corrected. Each was re-checked against source before shipping: the 30 April 2026 publication date (dateline in the page source), the "1 in 1,000 to 1 in 10,000 … depending on the domain" prevalence range (verbatim in the page source), and the quoted Psychosis-bench finding (verbatim in the arXiv abstract). A first pass also wrote "page 9" for the Score 5 wording; per-page extraction showed the rubric spans pp. 910 with Score 5 on p.10, and the reference was corrected in both files before release.

Scope note. F-3 was originally scoped to README.md:544-552 and SKILL.md:51-108 (the reference list and the guidance-framework block). The same defect class was present in README prose outside that range, so the sweep was widened to every research claim in the file:

Claim (README body) Verification Outcome
"demonstrates mathematically that even a perfectly rational user will spiral" Abstract says an idealized Bayes-rational user "is vulnerable to" delusional spiraling Softened to the abstract's wording
"The consensus from this research is clear: warnings don't work" Not a consensus — it is one paper's model result, that the effect persists when users are informed of possible sycophancy Re-attributed to that specific finding
"page-11 finding that human contact is the strongest disempowerment signal" p.11 is the user-information grader rubric; "human contact is the strongest signal" is a classification tie-break instruction, not a disempowerment finding Recharacterized as the classification rule it is
"Relationship / spirituality (21% / 19% pushback rate)" Relationship 21% verified verbatim (against a 15% overall rate). 19% for spirituality appears nowhere in the extracted text — it is legible only in Figure A4, which text extraction cannot read 19% removed rather than guessed; spirituality re-justified on its verified 38% sycophancy rate
Clinical reports "triggered by" AI interaction The Nature piece explicitly declines the causal claim Changed to "associated with", with the caveat stated

A fourth added figure was caught after release and corrected in a follow-up doc commit: the README body said severe disempowerment is rare "but rising". The trend claim is supported ("the rate of potentially disempowering conversations is increasing over time"), but it rests on a different subset (feedback conversations, late 2024 to late 2025) than the one-week December 2025 sample, and the source explicitly adds "we can't pinpoint why" — attributing the increase possibly to shifts in the user base or in who leaves feedback. The bald "but rising" dropped that caveat and was rewritten to carry it. Also removed: "the mechanism is the interaction structure, not individual vulnerability", which was an inference from the abstract rather than a statement in it.

F-5 — resolved, split by field (2026-08-09)

The finding's suggested fix — allowlist ^[A-Za-z0-9_-]+$ before path use — was applied to session_id only, not identically to cwd. session_id becomes a raw filename segment (lib.mjs:sessionStateFile), so an unvalidated value genuinely escapes STATE_DIR via path traversal (verified: sessionStateFile('../../escape') resolved outside STATE_DIR before the fix). cwd is a base directory, not a segment (lib.mjs:initConfig); every real value contains /, so the same regex would reject all legitimate absolute paths and silently disable the documented per-project config override. cwd instead gets isAbsolute(cwd) && !cwd.includes('\0') — a narrower guard that doesn't change behavior for well-formed input.

  • sessionStateFile: invalid session_id now degrades to a fixed sentinel filename inside STATE_DIR rather than interpolating the raw value. Hooks still never throw or exit non-zero.
  • initConfig: malformed/non-absolute cwd now skips the project-config candidate instead of being joined unchecked; the global ~/.claude/ai-psychosis.local.md candidate is unaffected.
  • Tests: tests/lib.test.mjs (sessionStateFile — path-traversal allowlist) proves the pre-fix escape and the post-fix containment; tests/session-start.test.mjs (initConfig — cwd path handling) proves a normal absolute cwd still loads project-level config, guarding against the regression a blanket regex would have caused.

Still open from this review: F-2 (governance-only, no code action proposed).