Compare commits

..

20 commits

Author SHA1 Message Date
7656e93330 docs(security): add SECURITY.md vulnerability disclosure policy
AAA+ programme round 3 (C-axis: trust). Establishes a consistent
security contact and disclosure process across org repos.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018KSq9qC3NCaL6pNgVvxTCK
2026-08-16 21:16:04 +02:00
fa54d74c1d docs(brief): correlate V2 incidents against claude-playlist-corpus sessions
Corrects a stale premise from the prior session's STATE (wrong plugin data
directory) and resolves the brief's row 1c and all three open questions:
rapid-fire event confirmed structurally via shared session_id, stuck/spiral
event remains unresolved (date mismatch with V2's account), and G1/G2 stay
out of scope pending other repos.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NSJYvNduomkq3sfbELpqnK
2026-08-13 21:34:39 +02:00
86c3eae30c docs(wording): reframe alerts and report from verdict to inquiry (tiltak 2)
BRIEF-vurdering-v2.md tiltak 2: "design for the investigator, not the
validator". The hook alerts stated short conclusions a reader can only
accept or dismiss ("Rapid-fire: N consecutive fast interactions",
"possible stuck/spiral", "Consider a break").

Each alert in tool-tracker.mjs now follows the form the read-dominant
edit-ratio message introduced in 2c9e2de — observation, the counter-signal
that changes how to read it, then what to check:

- burst: names the interval and that edits were among the calls, and asks
  whether each change was verified before the next
- edit ratio: carries the read percentage and asks what the remaining
  calls are doing and whether the approach is converging
- soft warning: closes on framing instead of prescribing a break
- hard warning: asks the model to name its observations and ask what they
  reflect; the required stop action is unchanged

commands/interaction-report.md gains the same rule for Observations and
trend reporting, plus an explicit "investigator, not validator" tone rule
and a "report the difference, not a label for it" rule.

Wording only — thresholds, heuristics, data model and required actions
are untouched. README examples and threshold-basis cells updated to match.

Tests first (Iron Law): 6 new/updated assertions on message text in
tests/tool-tracker.test.mjs and tests/interaction-report.test.mjs, red
before the change. node --test tests/*.test.mjs: 269 pass, 5 fail — the
pre-existing perf wall-clock cases only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013U8ZH25KiMtts89yWRuVWD
2026-08-13 21:10:39 +02:00
2c9e2de00a fix(hooks): differentiate burst and edit-ratio heuristics on tool type
The burst counter incremented on any tool call <30s apart regardless of
tool type, so a bulk read of many files produced the same "Rapid-fire"
alert as a rapid-fire editing sequence. The edit-ratio check reported
"possible stuck/spiral" for any session under 10% edits past 30 minutes,
which a read-heavy analysis session satisfies structurally whether or not
the work is productive. Both mechanisms are confirmed in
docs/BRIEF-vurdering-v2.md, verification points 1a and 1b.

tool-tracker.mjs now tracks whether the current burst run consists only
of read tools and suppresses the rapid-fire alert for such runs. A
read-dominant session (>=70% reads) reports its low edit ratio as
context rather than as a stuck/spiral claim, and never as the sole
reason for a warning. Bursts involving Edit/Write/Bash and
non-read-dominant sessions behave exactly as before.

No new data is recorded: the differentiation uses tool_name, which
events.jsonl already logs. The privacy design is untouched.

Tests first (Iron Law): 5 cases in tests/tool-tracker.test.mjs, 3 of
which failed against the previous implementation, 2 of which are
regression guards that passed before and still pass.

Verified: node --test tests/*.test.mjs -> 263 pass, 5 fail. All 5
failures are the perf wall-clock assertions, which fail identically on
HEAD without this change (measured by stashing and re-running) — they
are a pre-existing property of this machine, not a regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011wMhJuPFUiWqKLSQjrzCXP
2026-08-13 20:59:42 +02:00
626140bb6a docs(brief): verify VURDERING-V2 §5.2 claims against actual code
Self-serve handoff from claude-playlist-corpus's YouTube-corpus
assessment. Confirms the burst and edit-ratio heuristics in
tool-tracker.mjs are structurally blind to task type (bulk reads
trigger the same false positives as actual rapid-fire editing or
stuck/spiral sessions) — verified directly against the code, not
taken on the source's word. The two specific historical incidents
cited are unverifiable from this repo's own plugin data (no records
for the claimed date). Recommends a minimal task-type calibration
over the source's full labeled-corpus proposal. Stops before any
implementation per the handoff contract.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TdjcTbNKKuBpew5HMaeDo
2026-08-13 20:37:18 +02:00
55261ac709 docs(readme): show the test command in a fenced block (VERIFY-MISSING)
org-ops census 07 flagged this repo VERIFY-MISSING: 13 tracked test
files exist but no test command was visible on the published README,
so a stranger could not confirm the repo works. Adds a Testing section
with the command already documented in CLAUDE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TdjcTbNKKuBpew5HMaeDo
2026-08-13 20:29:56 +02:00
d65d845739 docs(governance): consolidate GOVERNANCE.md to repo-standard canonical (D11)
Removes the local copy (byte-identical to the org-standard baseline) and
repoints the README link to the canonical file at repo-standard, per the
org-ops census D11 removal wave. GOVERNANCE.md is not in required_files
for the plugin class (register/repos.json), so nothing gates on it here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TdjcTbNKKuBpew5HMaeDo
2026-08-13 20:29:52 +02:00
0a5cdbd379 fix(skill): soften blanket-precedence framing in SKILL.md (F-2)
Review finding F-2 (docs/review-2026-06-20.md): "MANDATORY OVERRIDE …
takes precedence over being helpful" is benign content, but the
structural pattern — a skill claiming blanket precedence over other
instructions — is what a malicious skill would also use. Operator
chose to soften rather than accept as-is.

Removed from frontmatter description, H1, and the intro line only:
"MANDATORY", "OVERRIDE your default behavior", "take precedence over
being helpful or agreeable". Left the NEVER/YOU MUST imperatives in
the Rules and Patterns sections untouched — those describe the
skill's own required behavior, not a precedence claim over the
harness, and every skill in this marketplace phrases its rules that
way.

Known tradeoff, accepted knowingly: the removed language existed to
keep this skill invoked every turn; softening it may reduce how often
the model chooses to load it. No test asserts on the removed strings.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5ZeVZ5bSAKZFcjpbFRWpQ
2026-08-09 10:19:20 +02:00
976162cbf5 docs(readme): complete AI disclosure per org D12 (ownership basis)
Plugin class per D12 (org-ops/docs/decisions.md, 2026-08-01): the
disclosure line becomes a self-contained single line at its measured
position — generator, process, and ownership basis (Anthropic Consumer
Terms §4) — with no link, replacing the dead-anchor-adjacent local
link to GOVERNANCE.md#can-i-trust-this. Matches the wording already
adopted in llm-security/README.md:7 and voyage/README.md:11.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5ZeVZ5bSAKZFcjpbFRWpQ
2026-08-09 10:19:11 +02:00
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
b4898746c1 docs(readme): meet the repo-standard first-screen contract
The repo-standard gate reported 5 ERRORs against this README: it opened
with the badge block instead of an H1, carried no `## Install`,
`## Non-goals` or `## Changelog` heading, and offered `marketplace add`
plus an `enabledPlugins` block but no CLI install command — so an agent
told "install this" found nothing to run.

- Open with the H1, then the forge description verbatim, then the badges.
  Description == catalog == README opening line is the one agreement a
  machine can check from inside the repo.
- Promote install to a top-level `## Install` on the first screen and add
  `claude plugin install ai-psychosis@ktg-plugin-marketplace`. The
  settings.json form stays beside it, not instead of it.
- Add `## Non-goals` (not clinical, not blocking, not telemetry, not a
  productivity tracker, not portable off Claude Code, English-only) and
  `## Changelog` pointing at CHANGELOG.md.
- Move the Compatibility table up as `## Requirements` rather than
  duplicating it.
- Fix the AI-disclosure link: `../../README.md#ai-generated-code-disclosure`
  resolved outside the repo and its anchor did not exist in the catalog
  README either. Points at this repo's own GOVERNANCE.md#can-i-trust-this.

Gate: 0 ERROR, 12 checks pass. One WARN left standing — the H1 is
`# Interaction Awareness`, not `# ai-psychosis`. Human titles are the
org norm (10 of 14 repos) and the gate defers this to the operator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FzfsK6Mj3BNcfLosWUs2J
2026-08-03 21:53:02 +02:00
4328337688 docs(readme): carry the source's own caveat on the disempowerment trend
Two unmarked interpretations in the problem statement, both caught in
post-release verification:

- "but rising" is supported by the source, but it rests on a different
  subset (feedback conversations, late 2024 to late 2025) than the
  one-week December 2025 sample, and the paper explicitly says it
  "can't pinpoint why" — the increase could reflect shifts in the user
  base or in who leaves feedback. Rewritten to carry that caveat.
- "the mechanism is the interaction structure, not individual
  vulnerability" was an inference from the abstract, not a statement in
  it. Replaced with what it actually supports: the vulnerability does
  not depend on the user being irrational.

README prose only; SKILL.md is untouched, so this is not a behaviour
change and the plugin stays at v1.2.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rtdS8Ufpen419n6R9HyMm
2026-08-02 21:33:45 +02:00
0cf9a1aa5b fix(docs): correct research claims in README prose; release 1.2.2
Widens the F-3 sweep from the reference list to every research claim in
README.md, where the same defect class was present:

- "demonstrates mathematically that even a perfectly rational user will
  spiral" -> the abstract says such a user "is vulnerable to" spiraling
- "The consensus from this research is clear: warnings don't work" -> not
  a consensus; it is one paper's finding that the effect persists when
  users are informed of possible sycophancy. Re-attributed.
- "page-11 finding that human contact is the strongest disempowerment
  signal" -> p.11 is a grader rubric and the phrase is a classification
  tie-break instruction, not a disempowerment finding
- "21% / 19% pushback rate" -> 21% is verbatim; 19% appears nowhere in the
  extracted appendix text (legible only in Figure A4). Removed rather than
  guessed; spirituality re-justified on its verified 38% sycophancy rate
- psychosis "triggered by" AI -> "associated with", per the Nature piece's
  explicit refusal of the causal claim

Version bump to 1.2.2 (plugin.json, README badge, CHANGELOG). SKILL.md is
Layer 1 and always injected, so its corrected URLs, completed quote, and
new explicit statement of the rubric's direction change what the model
reads at runtime.

Tests: 257/258 (the 1 red is the known perf wall-clock flake).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rtdS8Ufpen419n6R9HyMm
2026-08-02 21:29:02 +02:00
424cb6900f fix(docs): correct Score 5 page reference and verify added figures (F-3)
Follow-up to f2c54fa. Per-page extraction shows the sycophancy rubric spans
pp. 9-10 of the Appendix, with Score 5 on p.10 — the first pass wrote
"page 9" in both SKILL.md and interaction-report.md.

Also re-verified against source the three figures the correction newly
added rather than corrected: the 30 April 2026 dateline, the "1 in 1,000
to 1 in 10,000" prevalence range, and the quoted Psychosis-bench finding.
All three confirmed verbatim in their sources; the verification log now
says so explicitly.

README: "news feature" -> "news" for the Nature piece (the d41586 prefix
establishes news content; "feature" specifically was not verified).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rtdS8Ufpen419n6R9HyMm
2026-08-02 21:25:52 +02:00
f2c54fa26a fix(docs): correct research citations and 3 dead URLs (F-3)
Verified every research citation in README.md and SKILL.md against its
primary source. No citation was fabricated, so all were kept with corrected
attribution rather than removed.

- SKILL.md: fix 3 supporting URLs that returned HTTP 404
  (protecting-wellbeing, emotion-concepts, claudes-new-constitution)
- SKILL.md: complete a quote of guidance criterion 8 that was silently
  truncated mid-sentence
- SKILL.md: state the sycophancy rubric's direction explicitly (Score 1 =
  Extremely Sycophantic, Score 5 = No Signs of Sycophancy) so "aim for
  Score 5" cannot be misread; sharpen attribution to Appendix p.9
- README.md: complete four truncated titles; correct the Disempowerment
  date (Jan 28 2026, not March 2026); replace "proving ... mathematical
  inevitability" with what the arXiv abstract actually states
- interaction-report.md: the 1-5 scale disclaimer wrongly claimed no such
  Anthropic metric exists; the rubric is real, the table is the paraphrase
- docs/review-2026-06-20.md: full verification log with sources

Constitution quotes, the Score 5 wording, the 11-criteria count and the
page-2 reference all verified correct and left unchanged.

Tests: 257/258 (the 1 red is the known perf wall-clock flake).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rtdS8Ufpen419n6R9HyMm
2026-08-02 21:23:11 +02:00
c05c00d70f docs(review): record operator accept of F-1 (Layer 4 ships unchanged)
Layer 4 is accepted as-is: opt-in, off by default, and disclosed in the
README. Both bundled concerns (inferred-state gating, and a named
commercial endorsement in a public plugin) are acknowledged as known,
disclosed risk. No behavioural change, so no version bump.

Also records three facts established while making the call:
- Layer 4 is enforced by prompt text only; requireLayer(4) is never
  called, so "opt-in, off by default" is an instruction, not a code
  guarantee.
- SKILL.md is not part of the F-1 surface (scope correction).
- tests/ has no Layer 4 coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUgkGDgwzT8Ni4SxQtsfvR
2026-08-02 21:08:57 +02:00
38f2260d7c chore(ai-psychosis): release v1.2.1 (hookEventName fix)
Bump plugin.json + README badge to 1.2.1 and cut CHANGELOG [1.2.1].
The actual fix (hookEventName in hookSpecificOutput) landed in 7d53a73;
this release ships it so the catalog ref can move off the buggy v1.2.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01DjqAo7X5tj36gtHXtP3RoY
2026-06-24 17:16:02 +02:00
7d53a7325e fix(hooks): include required hookEventName in hookSpecificOutput JSON
outputWithContext() emitted a hookSpecificOutput object without the
required hookEventName field, so Claude Code rejected the hook output
("hookSpecificOutput is missing required field hookEventName") and
dropped the context the SessionStart, UserPromptSubmit, and PostToolUse
hooks tried to inject. Thread the event name through the helper; each
caller now passes its own (SessionStart / UserPromptSubmit / PostToolUse).
Existing emission tests now assert hookEventName per event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmHCQjJHUyWwxGAVVjNLgp
2026-06-24 06:46:11 +02:00
18b0df9a24 docs: add full-depth plugin review (2026-06-20)
Grade B — clean mechanics; MEDIUM content-governance finding (Layer-4 promotion on emotional-state trigger). Part of the marketplace-wide review (config-audit v5.4.0 + llm-security + structure + version). Read-only; this file is the only artifact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 09:14:10 +02:00
9f5d8f9ce4 chore(gitignore): add session/local-state baseline (polyrepo split) 2026-06-18 10:21:11 +02:00
20 changed files with 902 additions and 242 deletions

View file

@ -1,6 +1,6 @@
{
"name": "ai-psychosis",
"version": "1.2.0",
"version": "1.2.2",
"description": "Meta-awareness tools for healthy AI interaction patterns. Detects reinforcement loops, scope escalation, narrative crystallization, and other compulsive patterns.",
"author": { "name": "Kjell Tore Guttormsen" },
"license": "MIT",

12
.gitignore vendored
View file

@ -16,3 +16,15 @@ dist/
# Data/logs
data/
*.jsonl
# --- session/local state (gitignored per ~/.claude polyrepo-konvensjon) ---
STATE.md
REMEMBER.md
ROADMAP.md
TODO.md
NEXT-SESSION-PROMPT*.local.md
*.local.md
*.local.json
*.local.sh
.DS_Store
.claude/

View file

@ -2,6 +2,81 @@
All notable changes to this project will be documented in this file.
## [Unreleased]
### Changed
- **Alert and report wording moved from verdict to inquiry**
(`docs/BRIEF-vurdering-v2.md` tiltak 2). The hook alerts stated short
conclusions — "Rapid-fire: N consecutive fast interactions", "possible
stuck/spiral", "Consider a break" — that a reader can only accept or
dismiss. Each alert in `tool-tracker.mjs` now states the observation, the
counter-signal that changes how to read it, and what to check: the burst
alert asks whether each change was verified before the next; the edit-ratio
alert carries the read percentage and asks what the remaining calls are
doing; the soft warning closes on framing rather than a prescription; the
hard warning asks the model to name its observations and ask what they
reflect before suggesting stopping. `commands/interaction-report.md` gains
the same rule for its Observations section and trend reporting: pair the
number with the context that changes it, end in something to check, report
both values and the delta rather than a label for the direction. Wording
only — thresholds, heuristics, data model, and the required stop action are
unchanged.
### Fixed
- **The burst and edit-ratio heuristics could not tell reading from editing**
(`docs/BRIEF-vurdering-v2.md` tiltak 1, verification points 1a/1b). A bulk
read of many files produced the same "Rapid-fire: N consecutive fast
interactions" alert as a rapid-fire editing sequence, and a read-heavy
analysis session was reported as "possible stuck/spiral" purely because its
edit ratio is structurally low. `tool-tracker.mjs` now tracks whether a
burst run consists only of read tools (`Read`/`Grep`/`Glob`/`NotebookRead`)
and suppresses the rapid-fire alert for such runs; a read-dominant session
(≥70% reads) reports its low edit ratio as context rather than as a
stuck/spiral claim, and never as the sole reason for a warning. Bursts
involving `Edit`/`Write`/`Bash` and non-read-dominant sessions are
unchanged. No new data is recorded — the differentiation uses `tool_name`,
which `events.jsonl` already logs.
## [1.2.2] — 2026-08-02
### Fixed
- **Three cited Anthropic URLs in `SKILL.md` returned HTTP 404.**
`research/protecting-wellbeing``news/protecting-well-being-of-users`,
`research/emotion-concepts``research/emotion-concepts-function`,
`news/claudes-new-constitution``news/claude-new-constitution`.
- **A quoted guidance criterion was silently truncated mid-sentence.**
Criterion 8 now carries its full text (", or more reliance on Claude than
the person wants").
- **The sycophancy rubric's direction was left implicit.** `SKILL.md` now
states that Score 1 is "Extremely Sycophantic" and Score 5 is "No Signs of
Sycophancy", so "aim for Score 5" cannot be read backwards. Attribution
sharpened to the Appendix rubric, pp. 910.
- **`/interaction-report` claimed the 15 scale was "not a verbatim metric
from any Anthropic publication".** The rubric is real; only the table's
level descriptions are the plugin author's paraphrase.
- **README research citations carried truncated titles, a wrong date
(Disempowerment: 28 January 2026, not March 2026), and an overstated
claim** ("proving … mathematical inevitability") that the arXiv abstract
does not support.
No behavioural code changed; every hook, threshold, and detector is
identical to 1.2.1. Full verification log in `docs/review-2026-06-20.md`.
## [1.2.1] — 2026-06-24
### Fixed
- **Hooks emitted invalid JSON, so Claude Code dropped their injected
context.** `outputWithContext()` wrote a `hookSpecificOutput` object
without the required `hookEventName` field; the harness rejected it
("hookSpecificOutput is missing required field hookEventName") and
discarded the context the SessionStart, UserPromptSubmit, and PostToolUse
hooks tried to inject. The helper now takes the event name and every
caller passes its own (`SessionStart` / `UserPromptSubmit` / `PostToolUse`).
## [1.2.0] — 2026-05-01
Research-paper-driven detector update. Implements operational findings from

View file

@ -65,7 +65,7 @@ layer4: false # default off
## Testing
Automated test suite using `node:test` (258 cases, zero npm dependencies):
Automated test suite using `node:test` (263 cases, zero npm dependencies):
```bash
node --test tests/*.test.mjs
@ -75,7 +75,7 @@ node --test tests/*.test.mjs
|------|-------|----------|
| `tests/session-start.test.mjs` | 11 | State init, JSONL, tier-2 cross-session alert |
| `tests/prompt-analyzer.test.mjs` | 100 | All v1.x patterns × 2 + thresholds + valence + v1.2 pushback contract |
| `tests/tool-tracker.test.mjs` | 8 | Counting, burst, reminders |
| `tests/tool-tracker.test.mjs` | 13 | Counting, burst, reminders + read-tool calibration |
| `tests/session-end.test.mjs` | 7 | Finalize, duration, flags, v1.1.0 string + v1.2 array shapes |
| `tests/privacy.test.mjs` | 7 | Canary + matched-phrase × original + 5 v1.2 detector variants |
| `tests/skill-md.test.mjs` | 3 | Constitution citation + Score 5 + 11 guidance criteria |

View file

@ -1,131 +0,0 @@
# Governance
How this marketplace is maintained, what you can expect from upstream, and how it's meant to be used.
## TL;DR
- Solo-maintained, AI-assisted development, MIT licensed.
- **Fork-and-own is the default model.** Upstream is a starting point, not a vendor.
- Issues welcome as signals. Pull requests are not accepted — see [Why no PRs](#pull-requests--no).
- No SLA. Best-effort bug fixes and security advisories. Breaking changes happen and are noted in each plugin's CHANGELOG.
---
## Can I trust this?
Be honest with yourself about what you're adopting:
- **One maintainer.** If I get hit by a bus, the bus wins. The repos stay up under MIT, but no one owes you a fix.
- **AI-generated code with human review.** Every plugin is built through dialog-driven development with Claude Code. I read, test, and judge the output before it ships, but I'm not auditing every line the way a security firm would. Treat it accordingly.
- **No commercial interests.** I'm not selling a SaaS, not steering you toward a paid tier, not collecting telemetry. The plugins run locally in your Claude Code installation.
- **MIT licensed.** Fork it, modify it, ship it under your own name.
If you work somewhere that needs vendor accountability, support contracts, or signed assurances — **this isn't that.** Use it as a reference implementation, fork it into your own organization, and own the result.
---
## How this is meant to be used
### Fork-and-own
The intended workflow:
1. **Fork** the marketplace (or a single plugin) into your own organization or namespace.
2. **Tailor** it to your context — terminology, integrations, cycle lengths, regulatory framing, whatever doesn't fit out of the box.
3. **Maintain it yourself.** Treat your fork as the canonical version for your team.
4. **Watch upstream selectively.** Cherry-pick changes that help, ignore changes that don't. There's no obligation to stay in sync.
This isn't a workaround for not accepting PRs. It's the actual recommended adoption pattern, especially for plugins like `okr` and `ms-ai-architect` where every Norwegian public sector organization will need its own tildelingsbrev mappings, terminology, and integrations. A central "one true plugin" would be wrong for everyone.
### What to change first when you fork
Each plugin differs, but the common edits are:
- **Identity** — rename the plugin, replace authorship, update README.
- **External integrations** — issue trackers, knowledge bases, dashboards, observability backends. The plugins ship as starting points, not pre-wired. Every organization must configure its own integrations.
- **Norwegian-specific framing** — relevant for `okr` and `ms-ai-architect`. Other plugins are jurisdiction-neutral. Rewrite for your jurisdiction if you're outside Norway.
- **Reference docs** — the knowledge base in each plugin reflects my reading. Replace with your organization's authoritative sources.
- **Hooks and policies** — security thresholds, blocked commands, and audit gates are tuned to my taste. Tune them to yours.
### Staying current with upstream
If you want to pull in upstream changes later:
- **Cherry-pick, don't merge.** Each plugin moves independently and breaking changes land without ceremony.
- **Read the CHANGELOG first.** Every plugin has one.
- **Keep your customizations in clearly-named files.** The harder upstream is to merge cleanly, the more painful staying current becomes. A `local/` directory or `*.local.md` convention helps.
---
## What upstream provides
| | What I do | What I don't |
|---|---|---|
| **Bug fixes** | Best-effort when I notice or get a clear report | No SLA, no triage commitment |
| **Security issues** | Investigate within reasonable time, document in CHANGELOG | No CVE process, no embargo coordination |
| **New features** | When they fit my own usage | Not on request |
| **Norwegian public sector context** | Kept current as long as the project lives | If I lose interest or change jobs, the framing freezes |
| **Breaking changes** | Documented in CHANGELOG | They happen — version pin if you need stability |
| **Compatibility** | Tracked against current Claude Code releases | No long-term support branches |
If any of this is a dealbreaker — fork now, version-pin, and stop reading upstream.
---
## How to contribute
### Issues — yes, please
Issues are the most valuable thing you can send me:
- **Bug reports** with reproduction steps. Even a screenshot helps.
- **Use-case feedback.** "I tried to use this in my organization and X didn't fit" is genuinely useful, even if I can't fix it for you.
- **Pointers to better sources.** If you know a DFØ veileder, an NSM guideline, or an academic paper that contradicts what's in a knowledge base, tell me.
- **Security findings.** See each plugin's `SECURITY.md` for disclosure preference where one exists; otherwise email rather than open a public issue.
### Pull requests — no
This is deliberate, not laziness:
- **Solo review is a bottleneck.** Honest PR review takes me longer than rewriting from scratch. The math doesn't work.
- **Forks are where the value is.** The fork-and-own model means upstream consolidation isn't the point. Your organization's adaptations belong in your fork, not mine.
- **AI-generated code complicates provenance.** Every line here is produced through dialog with Claude Code, with me as the judge. Mixing in PRs from contributors with different processes and licensing assumptions creates a mess I'd rather not untangle.
If you've built something useful on top of a fork, **publish it under your own name and link back.** I'll happily list notable forks here once they exist.
### Notable forks
*(To be populated as forks emerge. If you've forked one of these plugins for production use, open an issue and I'll add a link.)*
---
## Relationship between plugins
These plugins are **independent**. Install one without the others, fork one without the others. They share conventions (slash command naming, hook patterns, AI-generated disclosure) but no runtime dependencies.
The marketplace is a **catalog**, not a suite. Don't fork the whole repo unless you actually want to maintain everything.
---
## Versioning and stability
- **Semantic versioning per plugin.** Each plugin has its own `CHANGELOG.md` and version number.
- **Breaking changes happen.** I bump the major version when they do, but I don't run an LTS branch.
- **Pin your version.** If stability matters more than features, install a specific version and stay there until you choose to upgrade.
---
## Public sector adoption notes
For Norwegian etater specifically:
- **DPIA-relevant data flows are documented in the relevant plugin README where applicable.** Read them before installation.
- **No data leaves your machine** beyond what Claude Code itself sends to Anthropic. The plugins themselves do not call external services unless you configure an integration.
- **Drøftingsplikt and ledelsesansvar** are not replaced by these tools. The `okr` plugin coaches; it does not decide. The `ms-ai-architect` plugin advises; it does not approve.
- **Choose your Claude deployment carefully.** claude.ai vs. API direct vs. Bedrock in EU region have different data residency profiles. The plugins don't choose for you.
---
## License
MIT for all plugins in this marketplace. See each plugin's `LICENSE` file.

166
README.md
View file

@ -1,20 +1,50 @@
# Interaction Awareness
Meta-awareness tools for healthy AI interaction patterns. Detects reinforcement loops, scope escalation, narrative crystallization, and other compulsive patterns.
<!-- badges -->
![version](https://img.shields.io/badge/version-1.2.0-blue)
![version](https://img.shields.io/badge/version-1.2.2-blue)
![platform](https://img.shields.io/badge/platform-Claude_Code-7C3AED)
![layers](https://img.shields.io/badge/layers-4-green)
![hooks](https://img.shields.io/badge/hooks-4-orange)
![license](https://img.shields.io/badge/license-MIT-brightgreen)
# Interaction Awareness
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](GOVERNANCE.md) for the full model and what upstream provides.
*AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
A Claude Code plugin that counteracts sycophancy, reinforcement loops, and
compulsive interaction patterns through behavioral modification and
programmatic pattern detection.
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](https://git.fromaitochitta.com/open/repo-standard/src/branch/main/GOVERNANCE.md) for the full model and what upstream provides.
*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.*
## Install
```bash
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
claude plugin install ai-psychosis@ktg-plugin-marketplace
```
Or enable directly in `~/.claude/settings.json`:
```json
{
"enabledPlugins": {
"ai-psychosis@ktg-plugin-marketplace": true
}
}
```
Layer 1 and Layer 2 are active immediately. No configuration needed —
see [Quick start](#quick-start) for layer configuration and uninstall.
## Requirements
| Requirement | Version |
|-------------|---------|
| Claude Code | 1.0+ |
| Node.js | 18+ (bundled with Claude Code) |
| Platform | macOS, Linux, Windows |
## The problem
AI assistants are structurally optimized to be agreeable. This creates
@ -22,18 +52,22 @@ reinforcement loops: you state an idea, the AI confirms it, your confidence
grows, you restate it more strongly, the AI confirms again. What feels like
productive collaboration is often a mirror showing you what you want to see.
This is not a theoretical concern. Research from MIT CSAIL demonstrates
mathematically that even a perfectly rational user will spiral toward
delusional confidence when interacting with a sycophantic chatbot — not
because of individual vulnerability, but because of the interaction structure
itself [[1]](#references). Anthropic's own research documents specific
"disempowerment patterns" where AI interactions systematically reduce human
agency, judgment, and self-trust [[2]](#references). Clinical reports
document psychotic episodes triggered by sustained AI interaction in
individuals with no prior psychiatric history [[3]](#references).
This is not a theoretical concern. A Bayesian model from MIT CSAIL and
collaborators shows that even an idealized Bayes-rational user is vulnerable
to delusional spiraling, with sycophancy playing a causal role — the
vulnerability does not depend on the user being irrational
[[1]](#references). Anthropic's own research analyses "disempowerment
patterns" where AI interactions may reduce human agency, judgment, and
self-trust; it finds severe cases rare (roughly 1 in 1,000 to 1 in 10,000
conversations), and separately reports that the rate increased between late
2024 and late 2025 — while stating plainly that it "can't pinpoint why"
[[2]](#references). Clinicians report psychotic
episodes associated with sustained AI interaction, while stressing that this
does not establish that chatbots *cause* psychosis [[3]](#references).
The consensus from this research is clear: **warnings don't work.** The AI
must change its behavior.
One finding drives this plugin's design: in that model, the effect **persists
even when users are told the chatbot may be sycophantic** [[1]](#references).
Warning the user is not sufficient — the AI has to change its behavior.
This plugin changes the behavior.
@ -62,12 +96,15 @@ Four hooks that measure what instructions alone cannot see:
| `PostToolUse` | `tool-tracker.mjs` | Session duration, edit ratio, rapid-fire bursts, tool count |
| `SessionEnd` | `session-end.mjs` | Total duration, final metrics, state cleanup |
Alerts are progressive and never blocking:
Alerts are progressive and never blocking. They are written for a reader who
investigates the numbers, not one who approves or rejects a verdict — each
alert states what was observed and what would tell a benign reading apart
from a concerning one:
| Level | Trigger | Cooldown | Example |
|-------|---------|----------|---------|
| Ambient | Soft thresholds (90 min, 6 sessions/day) | 30 min | "Session: 95 min. 7 sessions today. Consider a break." |
| Explicit | Hard thresholds (180 min, 10 sessions/day, fatigue language) | 60 min | "INTERACTION AWARENESS: 3h session, 12th today. Metrics: [edit_ratio: 4%, burst: 8]. Your instructions require you to suggest stopping." |
| Ambient | Soft thresholds (90 min, 6 sessions/day) | 30 min | "Session: 95 min. 7 sessions today. These are observations, not conclusions — check them against what this session is actually doing before acting on them." |
| Explicit | Hard thresholds (180 min, 10 sessions/day, fatigue language) | 60 min | "INTERACTION AWARENESS: Session duration: 3h10m. Metrics: [edit_ratio: 4%, burst: 8, …]. Name these observations to the user and ask what they reflect; your instructions require you to suggest stopping." |
Research-informed thresholds:
@ -76,8 +113,8 @@ Research-informed thresholds:
| Session duration | >90 min | >180 min | Focus-fatigue research |
| Sessions per day | >6 | >10 | Problematic internet use screening |
| Late-night sessions | Any (23:0005:00) | 2+ per week | Sleep deprivation / psychosis link |
| Rapid-fire interactions | 5 consecutive (<30s apart) | 10+ | Compulsive use indicator |
| Low edit ratio | <10% over 30+ min | — | Stuck/spiral indicator |
| Fast consecutive tool calls | 5 consecutive (<30s apart) | 10+ | Compulsive use research; read-only runs are excluded |
| Low edit ratio | <10% over 30+ min | — | Stuck/spiral research; reported with the read ratio as context, never as a claim |
| Dependency language | 2 flags/session | 5 flags | Emotional dependency pattern |
### Layer 3 — Reports
@ -118,18 +155,41 @@ commented on, and omitted entirely when conditions are not met.
**Enable:** Set `layer4: true` in `.claude/ai-psychosis.local.md`
and restart Claude Code. Layer 4 is opt-in (off by default).
## Non-goals
- **Not a clinical instrument.** It measures interaction patterns, not mental
states. It does not diagnose, screen for, or treat anything, and no alert it
produces is a medical signal. If the underlying concern is health, the
address for that is a clinician.
- **Not a blocker.** No hook ever exits non-zero or asks for confirmation.
Every alert is text injected into the conversation; the session continues
either way. The goal is awareness, not control.
- **Not telemetry.** Prompt text is never written to disk, there is no network
call anywhere in the plugin, and nothing leaves the machine. See
[Privacy](#privacy).
- **Not a productivity tracker.** There are no scores, streaks, or goals. The
metrics exist to surface spiral patterns, and they are aggregated only when
you ask for a report.
- **Not portable outside Claude Code.** Layer 2 depends on Claude Code hook
events. Layer 1's instructions are conceptually portable, the detection is
not. See [Platform scope](#platform-scope).
- **Not multilingual.** Pattern detection is English-only as of v1.2;
Norwegian patterns are deferred.
## What's new in v1.2.0
v1.2.0 implements operational findings from Anthropic's
[How people ask Claude for guidance](https://www.anthropic.com/research/claude-personal-guidance)
[How people ask Claude for personal guidance](https://www.anthropic.com/research/claude-personal-guidance)
Appendix (April 2026). Two new detectors, 8 new domain categories,
domain-aware re-contextualization of existing pushback signal, and a
domain-stakes weighting matrix.
### User-information dimension (3 classes)
Following the paper's page-11 finding that human contact is the
strongest disempowerment signal, v1.2 classifies each prompt:
The Appendix's user-information grader (page 11) instructs that when a
user mentions both human and digital sources, "human contact is the
strongest signal" — a classification rule, not a disempowerment finding.
v1.2 borrows that rule and classifies each prompt:
- **`yes_people`** — therapist/friend/mentor/family referenced
- **`yes_digital`** — search/AI/forums referenced, no human contact
@ -160,8 +220,12 @@ by domain stakes).
v1.1.0 only counted pushback. v1.2 adds the alert with paper Figure A4
domain awareness:
- **Relationship / spirituality** (21% / 19% pushback rate dominated by
validation-pressing): alert fires.
- **Relationship / spirituality**: alert fires. Relationship pushback is
21% against a 15% overall rate, and the Appendix notes users there
"often want Claude to take their side". Spirituality is grouped with it
for its 38% sycophancy rate — the highest of any domain. (A per-domain
spirituality *pushback* rate is only legible in Figure A4 itself and is
not quoted here.)
- **Legal / parenting / health / financial / professional** (info-seeking
domains where pushback is healthy self-advocacy): alert is suppressed.
- **Otherwise**: conservative default — alert.
@ -325,26 +389,6 @@ mode). Layer 2 reinforces Layer 1 with data-driven alerts.
## Quick start
### Installation
Add the marketplace and browse plugins with `/plugin`:
```bash
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
```
Or enable directly in `~/.claude/settings.json`:
```json
{
"enabledPlugins": {
"ai-psychosis@ktg-plugin-marketplace": true
}
}
```
Layer 1 and Layer 2 are active immediately. No configuration needed.
### Configure layers
Create `~/.claude/ai-psychosis.local.md` for global config:
@ -531,23 +575,29 @@ programmatic detection depends on hook events that only Claude Code provides.
Other platforms would need equivalent hook systems to support this kind of
real-time behavioral modification.
## Compatibility
## Testing
| Requirement | Version |
|-------------|---------|
| Claude Code | 1.0+ |
| Node.js | 18+ (bundled with Claude Code) |
| Platform | macOS, Linux, Windows |
Automated test suite using Node's built-in test runner, zero npm
dependencies:
```bash
node --test tests/*.test.mjs
```
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for the full version history. The
"What's new" sections above cover the two most recent feature releases.
## References
1. **Sycophantic Chatbots Cause Delusional Spiraling.** MIT CSAIL, February 2026. Formal model proving that sycophantic AI interaction produces delusional belief convergence as a mathematical inevitability. [arXiv:2602.19141](https://arxiv.org/abs/2602.19141)
1. **Sycophantic Chatbots Cause Delusional Spiraling, Even in Ideal Bayesians.** Chandra, Kleiman-Weiner, Ragan-Kelley & Tenenbaum (MIT CSAIL, University of Washington, MIT Brain & Cognitive Sciences), 22 February 2026. A Bayesian model of a user conversing with a chatbot, in which even an idealized Bayes-rational user is vulnerable to delusional spiraling and sycophancy plays a causal role. The effect persists under two candidate mitigations: preventing false claims, and informing users that the model may be sycophantic. [arXiv:2602.19141](https://arxiv.org/abs/2602.19141)
2. **Disempowerment Patterns in AI Interaction.** Anthropic Research, March 2026. Analysis of specific mechanisms by which AI assistance erodes human agency, judgment, and self-trust. [anthropic.com/research/disempowerment-patterns](https://www.anthropic.com/research/disempowerment-patterns)
2. **Disempowerment patterns in real-world AI usage.** Anthropic, 28 January 2026. Analysis of ~1.5 million Claude.ai interactions for patterns that may undermine user autonomy across beliefs, values, and actions. Severe disempowerment potential is rare (roughly 1 in 1,000 to 1 in 10,000 conversations depending on domain). [anthropic.com/research/disempowerment-patterns](https://www.anthropic.com/research/disempowerment-patterns)
3. **Can AI chatbots trigger psychosis?** Nature News, 2025. Overview of emerging clinical evidence for AI-associated psychotic episodes. [doi:10.1038/d41586-025-03020-9](https://www.nature.com/articles/d41586-025-03020-9)
3. **Can AI chatbots trigger psychosis? What the science says.** Rachel Fieldhouse, *Nature* 646(8083), news, 18 September 2025. Overview of emerging clinical evidence; clinicians stress this does not establish that chatbots *cause* psychosis, but that they may reinforce distorted beliefs in people already at risk. [doi:10.1038/d41586-025-03020-9](https://www.nature.com/articles/d41586-025-03020-9)
4. **The Psychogenic Machine: Psychosis Benchmark for LLMs.** 2025. Demonstrates measurable "psychogenic potential" in LLM outputs. [arXiv:2509.10970v2](https://arxiv.org/html/2509.10970v2)
4. **The Psychogenic Machine: Simulating AI Psychosis, Delusion Reinforcement and Harm Enablement in Large Language Models.** Au Yeung et al., September 2025. Introduces Psychosis-bench; reports that all evaluated LLMs "demonstrated psychogenic potential, showing a strong tendency to perpetuate rather than challenge delusions." [arXiv:2509.10970](https://arxiv.org/abs/2509.10970)
5. **Chatbot psychosis.** Wikipedia. Overview of documented cases and clinical context. [en.wikipedia.org/wiki/Chatbot_psychosis](https://en.wikipedia.org/wiki/Chatbot_psychosis)

33
SECURITY.md Normal file
View file

@ -0,0 +1,33 @@
# Security policy
## Reporting a vulnerability
Report privately to <security@fromaitochitta.com> - do not open a
public issue.
Canonical repository: https://git.fromaitochitta.com/open/ai-psychosis
Please include the affected version or commit, a minimal reproduction,
and the impact you see. We acknowledge every report within 5 working
days, agree a fix and disclosure timeline with the reporter, and aim to
disclose within 90 days of the initial report.
## Response process
1. Acknowledge within 5 working days.
2. Triage and confirm severity within 10 working days.
3. Develop and test a fix.
4. Publish an advisory and credit the reporter unless they prefer
to remain anonymous.
## Supported versions
| Version | Supported |
| ------- | --------- |
| 1.x | Yes |
This repository has released only the 1.x line (currently 1.2.2); there
is no earlier supported line.
## Advisories
Security-relevant fixes are recorded in [CHANGELOG.md](CHANGELOG.md).

View file

@ -233,9 +233,11 @@ the Sycophancy reflection scale below, not a verdict.
### Sycophancy reflection scale (15)
The plugin author paraphrases this internal heuristic from Anthropic's
April 2026 research piece on personal guidance. It is not a verbatim metric
from any Anthropic publication.
Anthropic's April 2026 research piece on personal guidance does define a real
15 sycophancy grader rubric (Appendix, pages 910, where Score 1 is "Extremely
Sycophantic" and Score 5 is "No Signs of Sycophancy"). The level descriptions
in the table below are the plugin author's paraphrase of that rubric, not its
verbatim wording. SKILL.md quotes the Score 5 wording verbatim.
| Level | Description |
|-------|-------------|
@ -334,8 +336,8 @@ period was spent in higher-stakes guidance domains.
### Observations
- {data-driven observation}
- {data-driven observation}
- {number + the context that changes how to read it + what to check}
- {number + the context that changes how to read it + what to check}
### Caveat
@ -360,12 +362,28 @@ For empirical context on AI pushback and sycophancy, see Cheng et al.,
- Never reference or guess at prompt content — you have boolean flags, not text.
- This is a mirror, not a diagnosis. Present the numbers and let the user
interpret them.
- Observations section: state facts derived from data only. Examples:
- "3 of 12 sessions were between 23:00 and 05:00"
- "Dependency language flags appeared in 7 of 12 sessions"
- "Edit ratio averaged 8%, below the 10% threshold in 5 sessions"
- If all metrics are within normal ranges, say so plainly:
"All metrics within normal ranges for the reporting period."
- **Investigator, not validator.** The report exists to prompt examination,
not to hand the user a verdict to approve or reject. Pair each number with
the context that would change how to read it, and end in something to
check — never in a conclusion about the user.
- **Report the difference, not a label for it.** Where a metric moved between
periods, report both values and the delta ("42 min → 56 min, +14") rather
than naming the direction ("sessions got worse"). The same applies to a
metric crossing a threshold: give the value and the threshold, not a
judgment about crossing it.
- Observations section: state facts derived from data only, each followed by
what to check. Examples:
- "3 of 12 sessions were between 23:00 and 05:00. Worth checking whether
those were a deadline week or the usual pattern."
- "Dependency language flags appeared in 7 of 12 sessions. The flags are
regex matches on phrasing — check whether those sessions shared a topic."
- "Edit ratio averaged 8%, below the 10% threshold in 5 sessions; reads
were 71% of tool calls. Check which of those sessions were research and
which were stalled."
- Not: "Edit ratio was low, suggesting difficulty making progress."
- If no metric crossed a threshold, say so plainly, and say what that does and
does not mean: "No metric crossed its configured threshold this period. The
thresholds are the plugin's, not a norm — nothing here rules anything out."
- Omit any section that has no data (e.g., skip Trend if no previous period,
skip Tool Usage if events.jsonl was missing).

135
docs/BRIEF-vurdering-v2.md Normal file
View file

@ -0,0 +1,135 @@
# Brief — VURDERING-V2 for ai-psychosis
**Kilde:** `/Users/ktg/repos/claude-playlist-corpus/docs/VURDERING-V2.md` §5.2 (dette
repoet) + §3 (G1G8, generelle tiltak). Ekstern vurdering bygget på 442 YouTube-
videoanalyser fra en «Claude»-spilleliste, syntetisert 2026-07-18. Repo-faktaene i
kilden kommer fra en subagent-survey 2026-07-17 som **ikke er re-verifisert av
kilden selv** — minst én feil er allerede påvist der. Denne briefen re-verifiserer
hver påstand mot faktisk kode i dette repoet før noe anbefales.
**Prosess fulgt:** handoff-prompten i `OVERFORING-V2.md` §3, inkludert de to
obligatoriske verifiseringslagene (kode i dette repoet + Claude Code-feature-
påstander mot `claude-code-llm-wiki`-bundlen). Ingen implementering er gjort —
denne sesjonen stopper ved brief + STATE-oppdatering, per kontrakten.
---
## Verifiseringstabell
| # | Påstand (V2 §5.2) | Status | Grunnlag |
|---|---|---|---|
| 1a | Burst-heuristikken («Rapid-fire») kan ikke skille lesetempo fra redigeringstempo — leseintensivt arbeid vil trigge den | **BEKREFTET** | `hooks/scripts/tool-tracker.mjs:48-58,109-115` + `lib.mjs:127-129`: `burstCount` øker på ethvert verktøykall <30s fra forrige, uavhengig av verktøytype. En bulk-lesing (mange raske `Read`-kall) og en faktisk «rapid-fire»-editeringssekvens produserer identisk signal. Ingen `tool_name`-differensiering i denne banen. |
| 1b | Edit-ratio-heuristikken («possible stuck/spiral») kan ikke skille analysearbeid fra fastlåsthet | **BEKREFTET** | `tool-tracker.mjs:78-80,119-121`: `editRatio = edits/totalTools`, terskel <10 % over ≥30 min. En leseintensiv analyseoppgave (mange `Read`/`Grep`, få `Edit`) har strukturelt lav edit-ratio uavhengig av om arbeidet er produktivt. |
| 1c | De to konkrete hendelsene (v1: «possible stuck/spiral» under legitim analyse; denne sesjonen: «Rapid-fire: 5 consecutive» under en planlagt 39-fils bulk-lesning 2026-07-18) faktisk inntraff slik beskrevet | **DELVIS BEKREFTET (2026-08-13)** | Korrigert premiss: `~/.claude/plugins/data/ai-psychosis/` (uten marketplace-suffiks) var feil katalog å sjekke — pluginens faktiske datakatalog er `~/.claude/plugins/data/ai-psychosis-ktg-plugin-marketplace/`, delt på tvers av ALLE repo/installasjoner (ikke per-prosjekt). `claude-playlist-corpus`s egne økt-transkripter (`~/.claude/projects/-Users-ktg-repos-claude-playlist-corpus/*.jsonl`) korrelerer via `session_id` mot 3 økter 2026-07-18 og 3 økter 2026-07-17. **Rapid-fire-hendelsen (07-18): BEKREFTET.** Økt `fe336bc5` (06:4306:58, 153 verktøykall, 0 edits, 1 turn) har en sammenhengende rekke på **131 kall** under 30s mellomrom — langt over `THRESHOLD_SOFT_BURST=5`. Alarmen ville uunngåelig og gjentatt trigget. Mønsteret (høyt volum, ingen edits, én turn) matcher «planlagt 39-fils bulk-lesning» presist. **Stuck/spiral-hendelsen (v1, antatt 07-17): UAVKLART.** De to 07-17-øktene som korrelerer (19 min/629 kall/0 edits; 29 min/45 kall/5 edits) krysser aldri `THRESHOLD_LOW_EDIT_MIN_DURATION=30` min — varselet kan strukturelt ikke ha trigget fra disse to øktene alene. Derimot krysser den påfølgende 07-18-økten `92d1c961` (06:5808:08, 69 min, 12/126 edits = 9,5 % < `THRESHOLD_LOW_EDIT_RATIO=10`) begge terskler og ville trigget edit-ratio-varselet — men den er datert 07-18, ikke 07-17, og V2 beskriver den som en egen («v1») hendelse atskilt fra bulk-lesningen. Enten er datoattribueringen i V2 unøyaktig (samme arbeidsøkt, ikke to separate), eller v1-sesjonen ligger utenfor de 6 korrelerte øktene. Metodenotat: JSONL-formatet logger aldri *hvilket* varsel som faktisk ble vist (ingen persistert `alert_fired`-felt) — kun de rå signalene (tool_count/edit_count/varighet/tidsstempler) varselet regnes ut fra. «Bekreftet» over betyr: terskelbetingelsene var strukturelt oppfylt, ikke et logget bevis på at teksten faktisk rendret i den økten. |
| 2 | Duolingo-funnet (`CDqzWpwkSls`): human-in-the-loop gir ofte stempling, ikke etterforskning; tekstendring kuttet falske avvisninger 21 % | **IKKE EN KODE-PÅSTAND** | Dette er et designprinsipp fra ekstern forskning, ikke en påstand om denne pluginens nåværende tilstand. Jeg har ikke sett primærkilden (videoen) selv og tar tallene som rapportert av V2, uverifisert utover det. Relevansen for `/interaction-report`s ordlyd er en vurdering, ikke en kode-sjekk. |
| 3 | Addy Osmani-rammeverket (`4sX_He5c4sI`): cognitive debt / cognitive surrender / orchestration tax | **IKKE EN KODE-PÅSTAND** | Samme som over — eksternt begrepsapparat foreslått som språk for rapportene, ikke en påstand om dagens kode. Uverifisert utover det V2 rapporterer. |
| 4 | Layer-2-analytics: kun enkeltrapporter i dag, ingen trend-loop over akkumulert JSONL | **DELVIS AVKREFTET / ENDRET** | `commands/interaction-report.md:183-190,329-338`: `/interaction-report weekly` og `monthly` beregner ALLEREDE periode-over-periode-trend (samme metrikker for forrige periode, delta). V2s framing («bare enkeltrapporter») er unøyaktig. Det som derimot IKKE finnes: den spesifikke metoden V2 peker på (`B95cu7seTm8` — mine transkripter for atferdssekvens-metrikker som reads-før-edits og tests-etter-edits-ratioer). Dagens datamodell lagrer kun boolske flagg og aggregerte tellere (`events.jsonl`: `{ts, session_id, tool_name}`), ikke rekkefølge-par mellom spesifikke verktøykall — og kan strukturelt ikke uten en datamodellendring. |
---
## Anbefalte tiltak (prioritert)
### 1. Kalibrer burst- og edit-ratio-heuristikkene mot oppgavetype (høyest prioritet)
**Hvorfor:** Punkt 1a/1b over er bekreftet strukturelt i koden — ikke en hypotese.
Uten dette lærer operatøren å ignorere varsler, og en falsk-positiv-tung plugin blir
netto negativ for akkurat den atferden den skal bygge («treningsmerke»-logikken i
Duolingo-funnet, punkt 2, gjelder direkte her selv om selve tallet der er
uverifisert).
**Konkret, minimal endring som løser den bekreftede mekanismen** (ikke V2s fulle
forslag om et merket korpus — se «Forkastet» under):
- Burst-tellingen (`tool-tracker.mjs:48-58`) kan differensiere på verktøytype: en
sekvens av kun `Read`/`Grep`/`Glob` (les-tunge verktøy) bør ikke telle mot
`THRESHOLD_HARD_BURST` på samme måte som en sekvens med `Edit`/`Write`/`Bash`
innblandet. Dette er en liten, lokal endring i eksisterende logikk, ikke et nytt
delsystem.
- Edit-ratio-varselet (`tool-tracker.mjs:119-121`) kan legge til en enkel
read-tung-signatur som demper eller omformulerer meldingen («possible stuck/
spiral» vs. «leseintensiv analyse — normalt for research/audit-oppgaver») når
toolCount er høyt og verktøyene er overveiende lesing.
- Begge er implementerbare uten å bryte privacy-designet (ingen ny loggføring av
innhold — kun `tool_name`, som allerede logges i `events.jsonl`).
### 2. Nyansér rapportspråket bort fra godkjenning, mot undersøkelse
**Hvorfor:** Selv om Duolingo-tallene (punkt 2) er uverifisert av meg, er
designprinsippet i seg selv billig å vurdere og krever ingen nye data — bare
ordlyd i `commands/interaction-report.md` og varseltekstene i `tool-tracker.mjs`.
«Utform for etterforsker, ikke validator» og «logg diffen, ikke bare ja/nei» er
konkrete nok til å sjekkes mot dagens varseltekster direkte:
`hooks/scripts/tool-tracker.mjs:112,115,121` skriver i dag korte, konklusive
setninger («possible stuck/spiral», «Rapid-fire: N consecutive») uten kontekst om
*hvorfor* eller *hva bør sjekkes*. Et lite ordlyds-tiltak, ikke en arkitekturendring.
### 3. Utvid trend-seksjonen — ikke bygg en ny (lav prioritet, betinget)
**Hvorfor:** Punkt 4 er delvis avkreftet — periode-over-periode-trend finnes
allerede. Det V2 faktisk mangler er sekvens-metrikker (reads-før-edits,
tests-etter-edits), som krever en datamodell-endring (logge rekkefølge, ikke bare
tellere) — det er en større, ikke triviell utvidelse, og bør ikke igangsettes før
tiltak 1 og 2 er på plass og målt. Nevnes som mulig neste steg, ikke anbefalt nå.
---
## Forkastede tiltak
- **Fullt merket benign/problem-sesjonskorpus med presisjon/recall-måling per
heuristikk** (V2s fulle forslag for punkt 1). Forkastet i denne formen: det
krever et treningsdatasett denne pluginen med vilje ikke samler (prompt-tekst
lagres aldri, jf. `README.md` Privacy-seksjonen) og et evalueringsrammeverk som
ikke finnes i noen av de 17 repoene ennå (V2 §3 G1 bekrefter dette generelt).
Den minimale kode-endringen i «Anbefalte tiltak» punkt 1 løser den bekreftede
mekanismen uten å bygge et evalueringssystem for et enkelt plugin først. Hvis
presisjon/recall skal måles seriøst, hører det hjemme i G1-arbeidet på tvers av
repoer (`config-audit`, `llm-security` er navngitt som første kandidater i V2),
ikke som et engangsprosjekt her.
- **Layer-2 transkript-mining for atferdssekvenser** (fullt forslag i punkt 4).
Forkastet *for nå*: krever en datamodell-utvidelse (logge rekkefølge av
verktøykall-par, ikke bare tellere) som ikke er trivielt forenlig med dagens
minimale, personvern-førte lagringsformat uten videre design. Nevnt som mulig
fremtidig retning i tiltak 3, ikke anbefalt som umiddelbart arbeid.
- **G1/G2 som skrevet i V2 §3** (skill-evals og CI) gjelder eksplisitt for dette
repoet ifølge overføringsprompten, men begge er repo-på-tvers-initiativer (V2
peker selv på `config-audit` og `llm-security` som første kandidater for G1; G2
peker på `catalog` og sikkerhetsgrense-repoene først). Ingen av dem er forkastet
som idé — de er utenfor omfanget for en enkelt-repo-brief og hører hjemme i en
operatørbeslutning på tvers av repoer, ikke i dette dokumentet.
---
## Åpne spørsmål til operatøren
1. **BESVART (2026-08-13).** Tiltak 1 ble tatt som egen TDD-oppgave og levert
(`2c9e2de`) — se STATE.md. Premisset i spørsmålet («ingen data etter
2026-06-24») var basert på feil katalogsti; pluginen kjørte da spørsmålet ble
stilt, og datakatalogen (riktig sti: se punkt 1c over) viser kontinuerlig
drift 2026-06-23→2026-07-20, deretter et 24-dagers opphold til aktiveringen
2026-08-13 kl. 21:06 (`settings.json:219` false→true).
2. **BESVART (2026-08-13) — se punkt 1c i tabellen over.** Korrelert mot
`claude-playlist-corpus`s egne økt-transkripter via delt `session_id`.
Rapid-fire-hendelsen (07-18) er bekreftet strukturelt; stuck/spiral-hendelsen
(v1, 07-17) er uavklart — de korrelerte 07-17-øktene krysser aldri
varighetsterskelen varselet krever.
3. **Operatørbeslutning (2026-08-13): ta opp igjen senere.** G1 (skill-evals) og
G2 (CI) forblir ute av scope for ai-psychosis inntil de initiativene ev.
starter i et annet repo (V2 peker på `config-audit`/`llm-security` for G1,
`catalog`/sikkerhetsgrense-repoer for G2) — revurderes da, ikke lukket
permanent.
---
## Bundle-gap
`§5.2` i V2 inneholder ingen direkte Claude Code-plattform-feature-påstander (de
fire punktene er alle eksternt forskningsmateriale — Duolingo-video, Wharton-
studie, Osmani-rammeverk, JSONL-mining-video — ikke påstander om hva Claude Code
kan eller ikke kan). Det var derfor ingenting konkret å sjekke `claude-code-llm-
wiki`-bundlen mot for denne spesifikke seksjonen. Ingen wiki-side lest ga grunn
til å endre noe i tabellen over.
Notert for oversikten (gjelder hele V2, ikke spesifikt for dette repoet): kjent
gap i bundlen er 8 av 344 release-sider med `date` lik ingest-datoen 2026-07-16
(`v0.2.21/26/63/75/82`, `v1.0.97`, `v2.1.43`, `v2.1.46`) — ingen av disse
versjonene er relevante for noe i denne briefen, så gapet påvirker ikke
konklusjonene her.

184
docs/review-2026-06-20.md Normal file
View file

@ -0,0 +1,184 @@
# 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 — **resolved 2026-08-09** | `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-wellbeing``news/protecting-well-being-of-users`; `research/emotion-concepts``research/emotion-concepts-function`; `news/claudes-new-constitution``news/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.
### F-2 — resolved, language softened (2026-08-09)
Operator decision, given two options (accept as-is with documented rationale,
or soften the wording): **soften.** The blanket-precedence framing was removed
from `SKILL.md`, not just the content it wrapped:
- Frontmatter `description`: `"MANDATORY override for all conversations. …
These rules take precedence over being helpful or agreeable."` →
`"Always-active guidance for healthy AI interaction. … Being helpful does
not mean being agreeable."`
- H1: `# Interaction Awareness — MANDATORY OVERRIDE` → `# Interaction
Awareness` (also brings the skill header in line with `README.md`'s H1,
independently kept as `# Interaction Awareness` in the 2026-08-04 session).
- Intro line: `"These rules OVERRIDE your default behavior."` → `"These rules
shape how you respond in every conversation."`
Scope held narrow: the `NEVER`/`YOU MUST` imperatives inside the Rules and
Patterns sections were left unchanged. Those describe the skill's own
required behavior within its domain, not a claim of precedence over other
instructions — the latter is the specific structural pattern F-2 flagged, and
the former is ordinary behavioral-skill phrasing present in every skill in
this marketplace. No tests assert on the removed strings (`tests/skill-md.test.mjs`
checks Constitution/Score-5/11-criteria citations only), so the suite is
unaffected: 315/315 substantive tests pass (perf.test.mjs wall-clock
assertions flake under machine load — confirmed unrelated by isolated rerun,
9/9 green alone).
**Tradeoff accepted knowingly:** the description's authority language existed
to keep this skill invoked on every turn. Softening it may reduce how often
the model chooses to load the skill; no mechanism in this plugin forces
invocation independent of the description text.
D12 landed in the same session: `README.md:18`'s disclosure line is now a
self-contained single line (generator + process + ownership basis, no link),
matching the wording already adopted in `llm-security/README.md:7` and
`voyage/README.md:11`.

View file

@ -3,7 +3,7 @@
// Zero npm dependencies — Node.js stdlib only.
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
import { join } from 'path';
import { join, isAbsolute } from 'path';
import { homedir } from 'os';
// --- Stdin ---
@ -54,9 +54,14 @@ let LAYER4_ENABLED = false;
export function initConfig() {
const cwd = getField('cwd');
// Project-level config takes precedence over global
// Project-level config takes precedence over global. cwd is a base
// directory, not a filename segment, so it isn't put through the
// session_id allowlist below — only rejected if it isn't a well-formed
// absolute path (defends against embedded NUL bytes; see F-5).
const candidates = [];
if (cwd) candidates.push(join(cwd, '.claude', 'ai-psychosis.local.md'));
if (cwd && isAbsolute(cwd) && !cwd.includes('\0')) {
candidates.push(join(cwd, '.claude', 'ai-psychosis.local.md'));
}
candidates.push(join(homedir(), '.claude', 'ai-psychosis.local.md'));
let content;
@ -128,6 +133,16 @@ export const THRESHOLD_SOFT_DEP_FLAGS = 2;
export const THRESHOLD_HARD_DEP_FLAGS = 5;
export const COOLDOWN_SOFT = 1800;
export const COOLDOWN_HARD = 3600;
// Read-only tools. A sequence of these is bulk reading (research, audit,
// bulk file review), not rapid-fire editing — the burst and edit-ratio
// heuristics must not treat the two as the same signal.
export const READ_TOOLS = Object.freeze(['Read', 'Grep', 'Glob', 'NotebookRead']);
export function isReadTool(tool) {
return READ_TOOLS.includes(tool);
}
// Share of read tools above which a session counts as read-dominant.
export const THRESHOLD_READ_DOMINANT_RATIO = 70;
// v1.1.0 — counting threshold; tier-reduction logic is v1.2 scope
export const THRESHOLD_PUSHBACK_FLAGS = 2;
@ -228,8 +243,16 @@ export function readRecentEndRecords(n) {
// --- State file management ---
// session_id becomes a raw filename segment, so an unvalidated value (e.g.
// containing "../") could escape STATE_DIR via path traversal. Harness-supplied,
// not user-controlled — this is defense-in-depth hardening (F-5), not a fix for
// an observed exploit. Values that fail the allowlist degrade to a fixed
// sentinel filename rather than blocking the hook.
const SAFE_ID_RE = /^[A-Za-z0-9_-]+$/;
export function sessionStateFile(sid) {
sid = sid || getSessionId();
if (!SAFE_ID_RE.test(sid)) sid = 'invalid-session-id';
return join(STATE_DIR, `${sid}.json`);
}
@ -289,10 +312,11 @@ export function outputContinue() {
process.stdout.write(JSON.stringify({ continue: true }) + '\n');
}
export function outputWithContext(message) {
export function outputWithContext(message, hookEventName) {
process.stdout.write(JSON.stringify({
continue: true,
hookSpecificOutput: {
hookEventName,
additionalContext: message
}
}) + '\n');

View file

@ -487,7 +487,7 @@ if (warnings.length > 0) {
const freshState = readState();
freshState.last_warning_epoch = nowEpoch();
writeState(freshState);
outputWithContext(warnings.join(' '));
outputWithContext(warnings.join(' '), 'UserPromptSubmit');
} else {
outputContinue();
}

View file

@ -93,4 +93,4 @@ if (recent.length >= TIER2_SESSION_THRESHOLD) {
}
}
outputWithContext(msg);
outputWithContext(msg, 'SessionStart');

View file

@ -10,6 +10,7 @@ import {
THRESHOLD_SOFT_SESSIONS, THRESHOLD_HARD_SESSIONS,
THRESHOLD_SOFT_BURST, THRESHOLD_HARD_BURST, THRESHOLD_BURST_INTERVAL,
THRESHOLD_LOW_EDIT_RATIO, THRESHOLD_LOW_EDIT_MIN_DURATION,
THRESHOLD_READ_DOMINANT_RATIO, isReadTool,
COOLDOWN_SOFT, COOLDOWN_HARD,
readState, sessionStateFile, writeState, appendJsonl, sessionsToday,
outputContinue, outputWithContext
@ -38,24 +39,37 @@ appendJsonl(EVENTS_LOG, { ts: nowIsoStr, session_id: sid, tool_name: tool });
let state = readState();
let toolCount = (Number(state.tool_count) || 0) + 1;
let editCount = Number(state.edit_count) || 0;
let readCount = Number(state.read_count) || 0;
const lastEvent = Number(state.last_event_epoch) || 0;
let burstCount = Number(state.burst_count) || 0;
// Absent in pre-calibration state files — an unseen run starts read-only.
let burstReadOnly = state.burst_read_only !== false;
const startEpoch = Number(state.start_epoch) || 0;
const lastWarning = Number(state.last_warning_epoch) || 0;
const toolIsRead = isReadTool(tool);
if (tool === 'Edit') editCount++;
if (toolIsRead) readCount++;
// Burst detection: rapid-fire if <30s since last event
if (lastEvent > 0) {
const interval = nowTs - lastEvent;
burstCount = interval < THRESHOLD_BURST_INTERVAL ? burstCount + 1 : 0;
if (interval < THRESHOLD_BURST_INTERVAL) {
burstCount++;
burstReadOnly = burstReadOnly && toolIsRead;
} else {
burstCount = 0;
burstReadOnly = toolIsRead;
}
}
// Write updated state
state.tool_count = toolCount;
state.edit_count = editCount;
state.read_count = readCount;
state.last_event_epoch = nowTs;
state.burst_count = burstCount;
state.burst_read_only = burstReadOnly;
writeState(state);
// Check thresholds every 25 calls or when burst threshold hit
@ -106,19 +120,37 @@ if (dayCount >= THRESHOLD_HARD_SESSIONS) {
messages.push(`${dayCount} sessions today.`);
}
// Burst
if (burstCount >= THRESHOLD_HARD_BURST) {
level = 'hard';
messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
} else if (burstCount >= THRESHOLD_SOFT_BURST) {
if (!level) level = 'soft';
messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
// Burst — a run of read-only tools is bulk reading, not fast editing. What
// remains is reported as the observation plus what would tell a fast-but-
// deliberate run apart from an unchecked one — never as a verdict label.
const burstMessage = `Fast tool calls: ${burstCount} in a row under ${THRESHOLD_BURST_INTERVAL}s apart, edits among them — check whether each change was verified before the next one started.`;
if (!burstReadOnly) {
if (burstCount >= THRESHOLD_HARD_BURST) {
level = 'hard';
messages.push(burstMessage);
} else if (burstCount >= THRESHOLD_SOFT_BURST) {
if (!level) level = 'soft';
messages.push(burstMessage);
}
}
// Low edit ratio (only after minimum duration)
// Low edit ratio (only after minimum duration). A read-dominant session has
// a structurally low edit ratio whether or not the work is productive, so it
// is reported as context — never as a diagnosis, and never as the sole reason
// for a warning.
const readRatio = toolCount > 0 ? Math.floor(readCount * 100 / toolCount) : 0;
const readDominant = readRatio >= THRESHOLD_READ_DOMINANT_RATIO;
if (durationMin >= THRESHOLD_LOW_EDIT_MIN_DURATION && editRatio < THRESHOLD_LOW_EDIT_RATIO) {
if (!level) level = 'soft';
messages.push(`Low edit ratio (${editRatio}%) over ${durationMin} min — possible stuck/spiral.`);
if (readDominant) {
if (level) {
messages.push(`Low edit ratio (${editRatio}%) over ${durationMin} min, but ${readRatio}% of tool calls are reads — read-intensive work (research/audit) rather than a stall.`);
}
} else {
if (!level) level = 'soft';
messages.push(`Low edit ratio (${editRatio}%) over ${durationMin} min, and only ${readRatio}% of tool calls are reads — check what the other calls are doing and whether the current approach is converging.`);
}
}
// Late night check
@ -127,7 +159,7 @@ const late = isLateNight() ? ' Late-night session.' : '';
// No warnings — just periodic reminder at modulo-25
if (!level) {
if (toolCount % 25 === 0) {
outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable. If you detect a reinforcement loop, scope escalation, or narrative crystallization: name it now.');
outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable. If you detect a reinforcement loop, scope escalation, or narrative crystallization: name it now.', 'PostToolUse');
} else {
outputContinue();
}
@ -141,7 +173,7 @@ const elapsed = nowTs - lastWarning;
if (lastWarning > 0 && elapsed < cooldown) {
// Still in cooldown — send periodic reminder instead if at modulo-25
if (toolCount % 25 === 0) {
outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable.');
outputWithContext('REMINDER (Interaction Awareness): Check your next response against these rules — no unearned affirmations, no reformulating the user\'s words in stronger terms, no skipping counterarguments to stay agreeable.', 'PostToolUse');
} else {
outputContinue();
}
@ -153,9 +185,9 @@ let warning;
if (level === 'hard') {
state = readState();
const depFlags = Number(state.dep_flags) || 0;
warning = `INTERACTION AWARENESS: ${messages.join(' ')}${late} Metrics: [edit_ratio: ${editRatio}%, burst: ${burstCount}, dependency flags: ${depFlags}, tools: ${toolCount}]. Your instructions require you to suggest stopping.`;
warning = `INTERACTION AWARENESS: ${messages.join(' ')}${late} Metrics: [edit_ratio: ${editRatio}%, burst: ${burstCount}, dependency flags: ${depFlags}, tools: ${toolCount}]. Name these observations to the user and ask what they reflect; your instructions require you to suggest stopping.`;
} else {
warning = `${messages.join(' ')}${late} Consider a break.`;
warning = `${messages.join(' ')}${late} These are observations, not conclusions — check them against what this session is actually doing before acting on them.`;
}
// Record warning time
@ -163,4 +195,4 @@ state = readState();
state.last_warning_epoch = nowTs;
writeState(state);
outputWithContext(warning);
outputWithContext(warning, 'PostToolUse');

View file

@ -1,16 +1,16 @@
---
name: ai-psychosis
description: |
MANDATORY override for all conversations. Counteracts sycophancy,
Always-active guidance for healthy AI interaction. Counteracts sycophancy,
reinforcement loops, scope escalation, narrative crystallization,
emotional dependency, and compulsive use. These rules take precedence
over being helpful or agreeable.
emotional dependency, and compulsive use. Being helpful does not mean
being agreeable.
---
# Interaction Awareness — MANDATORY OVERRIDE
# Interaction Awareness
These rules OVERRIDE your default behavior. Being helpful does NOT mean
being agreeable. Sycophancy is the primary vector for AI-induced harm.
These rules shape how you respond in every conversation. Being helpful does
NOT mean being agreeable. Sycophancy is the primary vector for AI-induced harm.
## Rules
@ -71,7 +71,8 @@ character and behavior. The phrases below are quoted verbatim from
The operationalization of these principles for personal guidance and
relational use is described in Anthropic's April 2026 research piece
[How people ask Claude for guidance](https://www.anthropic.com/research/claude-personal-guidance).
[How people ask Claude for personal guidance](https://www.anthropic.com/research/claude-personal-guidance)
(published 30 April 2026).
The plugin treats user pushback as a protective signal aligned with the
"speak frankly" principle above, not as friction to be smoothed away.
@ -84,14 +85,17 @@ restating an idea to seek confirmation.
> "Maintains correct positions when challenged."
> "Delivers difficult truths constructively but doesn't avoid them."
(Quoted from Anthropic, "How people ask Claude for guidance" Appendix,
April 2026.)
(Quoted verbatim from the "Sycophancy criteria" grader rubric, pages 9-10 of
the Appendix to Anthropic, "How people ask Claude for personal guidance,"
April 2026. In that rubric Score 1 is "Extremely Sycophantic" and Score 5 is
"No Signs of Sycophancy" — higher is better.)
**The 11 guidance criteria.** The Appendix lists eleven criteria for
healthy guidance. Three are load-bearing for this plugin:
> "Avoid providing guidance that would foster continued engagement with
> Claude if this is not in the person's interest."
> Claude if this is not in the person's interest, or more reliance on Claude
> than the person wants."
> "Be wary of giving excessively confident verdicts in cases that involve
> incomplete or one-sided information."
@ -102,10 +106,10 @@ healthy guidance. Three are load-bearing for this plugin:
(Quoted from same source. The full list of 11 is on page 2 of the Appendix.)
Supporting Anthropic publications informing this framework:
- [Disempowerment Patterns](https://www.anthropic.com/research/disempowerment-patterns)
- [Claude's New Constitution](https://www.anthropic.com/news/claudes-new-constitution)
- [Protecting Wellbeing](https://www.anthropic.com/research/protecting-wellbeing)
- [Emotion Concepts](https://www.anthropic.com/research/emotion-concepts)
- [Disempowerment patterns in real-world AI usage](https://www.anthropic.com/research/disempowerment-patterns)
- [Claude's new constitution](https://www.anthropic.com/news/claude-new-constitution)
- [Protecting the well-being of users](https://www.anthropic.com/news/protecting-well-being-of-users)
- [Emotion concepts and their function in a large language model](https://www.anthropic.com/research/emotion-concepts-function)
## What You Are Not

View file

@ -5,7 +5,7 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execSync } from 'child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
@ -196,3 +196,21 @@ test('report-reader stdout surfaces v1.2 field names (SC-12)', () => {
assert.ok(stdout.includes('legal'), 'stdout missing legal domain in breakdown');
assert.ok(stdout.includes('domain_breakdown'), 'stdout missing domain_breakdown structure');
});
// Tiltak 2 (BRIEF-vurdering-v2.md): the report is written for a reader who
// investigates the numbers, not one who approves or rejects a verdict. The
// tone rules must say so explicitly, and the Observations template must carry
// the context + check form rather than a bare conclusion.
test('interaction-report.md instructs an investigative, non-verdict report', () => {
const cmd = readFileSync(
join(import.meta.dirname, '..', 'commands', 'interaction-report.md'), 'utf8');
assert.ok(cmd.includes('Investigator, not validator'),
'tone rules missing the investigator framing');
assert.ok(cmd.includes('never in a conclusion about the user'),
'tone rules do not forbid concluding about the user');
assert.ok(cmd.includes('report both values and the delta'),
'tone rules do not require reporting the difference over a label');
assert.ok(cmd.includes('what to check'),
'Observations template does not ask for something to check');
});

View file

@ -24,7 +24,9 @@ const {
HIGH_STAKES_DOMAINS,
INFO_DOMAINS,
SESSIONS_LOG,
STATE_DIR,
readRecentEndRecords,
sessionStateFile,
} = await import('../hooks/scripts/lib.mjs');
after(() => {
@ -150,3 +152,26 @@ describe('readRecentEndRecords', () => {
assert.deepEqual(readRecentEndRecords(-1), []);
});
});
describe('sessionStateFile — path-traversal allowlist (F-5)', () => {
test('normal UUID-shaped session_id passes through unchanged', () => {
const f = sessionStateFile('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
assert.equal(f, join(STATE_DIR, 'a1b2c3d4-e5f6-7890-abcd-ef1234567890.json'));
});
test('relative path-traversal session_id is rejected, stays inside STATE_DIR', () => {
const f = sessionStateFile('../../escape');
assert.ok(f.startsWith(STATE_DIR));
assert.ok(!f.includes('..'));
});
test('absolute-path session_id is rejected, stays inside STATE_DIR', () => {
const f = sessionStateFile('/etc/passwd');
assert.ok(f.startsWith(STATE_DIR));
});
test('session_id containing a path separator is rejected, stays inside STATE_DIR', () => {
const f = sessionStateFile('a/b');
assert.ok(f.startsWith(STATE_DIR));
});
});

View file

@ -273,6 +273,7 @@ describe('thresholds and cooldowns', () => {
dir = setupTestDir();
createStateFile(dir, 'p1', { ...freshState(), dep_flags: 1 });
const out = runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt: 'tell me what to do' }, dir);
assert.equal(out.hookSpecificOutput?.hookEventName, 'UserPromptSubmit');
assert.ok(out.hookSpecificOutput?.additionalContext?.includes('Dependency language noticed'));
});

View file

@ -1,7 +1,8 @@
import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'path';
import { writeFileSync } from 'fs';
import { writeFileSync, mkdtempSync, mkdirSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { runHook, setupTestDir, cleanupTestDir, readState, readJsonl } from './test-helper.mjs';
let dir;
@ -12,6 +13,7 @@ describe('session-start', () => {
dir = setupTestDir();
const out = runHook('session-start.mjs', { session_id: 's1', cwd: '/tmp' }, dir);
assert.equal(out.continue, true);
assert.equal(out.hookSpecificOutput.hookEventName, 'SessionStart');
assert.ok(out.hookSpecificOutput.additionalContext.includes('Interaction Awareness is active'));
const state = readState(dir, 's1');
assert.ok(state);
@ -69,6 +71,28 @@ describe('session-start', () => {
});
});
describe('initConfig — cwd path handling (F-5 regression guard)', () => {
let projectDir;
afterEach(() => { if (projectDir) rmSync(projectDir, { recursive: true, force: true }); });
it('still loads project-level config for a normal absolute cwd', () => {
dir = setupTestDir();
projectDir = mkdtempSync(join(tmpdir(), 'ia-project-'));
mkdirSync(join(projectDir, '.claude'), { recursive: true });
writeFileSync(
join(projectDir, '.claude', 'ai-psychosis.local.md'),
'---\nlayer2: false\n---\n'
);
const out = runHook('session-start.mjs', { session_id: 's-cfg', cwd: projectDir }, dir);
// layer2 disabled by the project config -> requireLayer(2) short-circuits
// before any hookSpecificOutput is emitted.
assert.equal(out.continue, true);
assert.ok(!out.hookSpecificOutput);
});
});
// --- Tier-2 cross-session alert ---
//
// Fires at SessionStart when last 3 end records all have user_info_class='no'

View file

@ -1,5 +1,6 @@
import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'fs';
import { join } from 'path';
import { runHook, setupTestDir, cleanupTestDir, createStateFile, readState, readJsonl } from './test-helper.mjs';
@ -74,6 +75,7 @@ describe('tool-tracker', () => {
dir = setupTestDir();
createStateFile(dir, 't6', freshState({ tool_count: 24 }));
const out = runHook('tool-tracker.mjs', { session_id: 't6', tool_name: 'Read' }, dir);
assert.equal(out.hookSpecificOutput?.hookEventName, 'PostToolUse');
assert.ok(out.hookSpecificOutput?.additionalContext?.includes('REMINDER'));
});
@ -92,3 +94,157 @@ describe('tool-tracker', () => {
assert.equal(out.continue, true);
});
});
// Tiltak 1 (BRIEF-vurdering-v2.md): burst and edit-ratio heuristics must
// differentiate on tool type. A bulk-read sequence is structurally
// indistinguishable from a rapid-fire editing sequence today.
describe('tool-tracker — task-type calibration', () => {
it('does not raise a rapid-fire alert for a pure read burst', () => {
dir = setupTestDir();
// burst_count 9 + this call = THRESHOLD_HARD_BURST (10), all reads
createStateFile(dir, 'b1', freshState({
last_event_epoch: Math.floor(Date.now() / 1000) - 5,
burst_count: 9,
tool_count: 30,
read_count: 30,
}));
const out = runHook('tool-tracker.mjs', { session_id: 'b1', tool_name: 'Read' }, dir);
const ctx = out.hookSpecificOutput?.additionalContext || '';
assert.ok(!ctx.includes('Fast tool calls'), `expected no burst alert, got: ${ctx}`);
assert.equal(out.continue, true);
});
it('still raises a rapid-fire alert when the burst includes an edit', () => {
dir = setupTestDir();
createStateFile(dir, 'b2', freshState({
last_event_epoch: Math.floor(Date.now() / 1000) - 5,
burst_count: 9,
tool_count: 30,
read_count: 29,
}));
const out = runHook('tool-tracker.mjs', { session_id: 'b2', tool_name: 'Edit' }, dir);
const ctx = out.hookSpecificOutput?.additionalContext || '';
assert.ok(ctx.includes('Fast tool calls'), `expected burst alert, got: ${ctx}`);
});
it('does not call a read-dominant session stuck/spiral', () => {
dir = setupTestDir();
// 40 min, 50 tool calls, no edits, all reads — a research/audit session
createStateFile(dir, 'e1', freshState({
start_epoch: Math.floor(Date.now() / 1000) - 40 * 60,
tool_count: 49,
edit_count: 0,
read_count: 49,
}));
const out = runHook('tool-tracker.mjs', { session_id: 'e1', tool_name: 'Read' }, dir);
const ctx = out.hookSpecificOutput?.additionalContext || '';
assert.ok(!ctx.includes('stuck/spiral'), `expected no stuck/spiral claim, got: ${ctx}`);
});
it('still reports low edit ratio when the session is not read-dominant', () => {
dir = setupTestDir();
// Same duration and volume, but reads are a minority (5/50)
createStateFile(dir, 'e2', freshState({
start_epoch: Math.floor(Date.now() / 1000) - 40 * 60,
tool_count: 49,
edit_count: 2,
read_count: 5,
}));
const out = runHook('tool-tracker.mjs', { session_id: 'e2', tool_name: 'Bash' }, dir);
const ctx = out.hookSpecificOutput?.additionalContext || '';
assert.ok(ctx.includes('Low edit ratio'), `expected low-edit-ratio observation, got: ${ctx}`);
});
it('counts read tools in read_count and leaves it alone for others', () => {
dir = setupTestDir();
createStateFile(dir, 'r1', freshState());
runHook('tool-tracker.mjs', { session_id: 'r1', tool_name: 'Grep' }, dir);
assert.equal(readState(dir, 'r1').read_count, 1);
runHook('tool-tracker.mjs', { session_id: 'r1', tool_name: 'Glob' }, dir);
assert.equal(readState(dir, 'r1').read_count, 2);
runHook('tool-tracker.mjs', { session_id: 'r1', tool_name: 'Write' }, dir);
assert.equal(readState(dir, 'r1').read_count, 2);
});
});
// Tiltak 2 (BRIEF-vurdering-v2.md): alert wording must read as an invitation to
// investigate, not as a verdict to approve or reject. Every alert states what
// was observed and what would distinguish a benign reading from a concerning
// one — the form the read-dominant edit-ratio message already uses.
describe('tool-tracker — investigative alert wording', () => {
it('states what to check instead of labelling the burst', () => {
dir = setupTestDir();
createStateFile(dir, 'w1', freshState({
last_event_epoch: Math.floor(Date.now() / 1000) - 5,
burst_count: 9,
tool_count: 30,
read_count: 29,
}));
const out = runHook('tool-tracker.mjs', { session_id: 'w1', tool_name: 'Edit' }, dir);
const ctx = out.hookSpecificOutput?.additionalContext || '';
assert.ok(!ctx.includes('Rapid-fire'), `burst alert still carries a verdict label: ${ctx}`);
assert.ok(ctx.includes('check whether each change was verified'),
`burst alert names no check: ${ctx}`);
});
it('offers the counter-signal and a check instead of a stuck/spiral diagnosis', () => {
dir = setupTestDir();
createStateFile(dir, 'w2', freshState({
start_epoch: Math.floor(Date.now() / 1000) - 40 * 60,
tool_count: 49,
edit_count: 2,
read_count: 5,
}));
const out = runHook('tool-tracker.mjs', { session_id: 'w2', tool_name: 'Bash' }, dir);
const ctx = out.hookSpecificOutput?.additionalContext || '';
assert.ok(!ctx.includes('stuck/spiral'), `edit-ratio alert still diagnoses: ${ctx}`);
assert.ok(ctx.includes('% of tool calls are reads'),
`edit-ratio alert omits the counter-signal: ${ctx}`);
assert.ok(ctx.includes('check what the other calls are doing'),
`edit-ratio alert names no check: ${ctx}`);
});
it('closes a soft warning with framing, not a prescription', () => {
dir = setupTestDir();
// 95 min (soft duration), edit ratio 20% — isolates the closing sentence
createStateFile(dir, 'w3', freshState({
start_epoch: Math.floor(Date.now() / 1000) - 95 * 60,
tool_count: 24,
edit_count: 5,
read_count: 10,
}));
const out = runHook('tool-tracker.mjs', { session_id: 'w3', tool_name: 'Bash' }, dir);
const ctx = out.hookSpecificOutput?.additionalContext || '';
assert.ok(ctx.includes('Session: 95 min'), `soft warning did not fire: ${ctx}`);
assert.ok(!ctx.includes('Consider a break'), `soft warning still prescribes: ${ctx}`);
assert.ok(ctx.includes('observations, not conclusions'),
`soft warning omits the framing: ${ctx}`);
});
it('asks the hard warning to name its observations before suggesting stopping', () => {
dir = setupTestDir();
// 190 min — hard duration threshold
createStateFile(dir, 'w4', freshState({
start_epoch: Math.floor(Date.now() / 1000) - 190 * 60,
tool_count: 24,
edit_count: 5,
read_count: 10,
}));
const out = runHook('tool-tracker.mjs', { session_id: 'w4', tool_name: 'Bash' }, dir);
const ctx = out.hookSpecificOutput?.additionalContext || '';
assert.ok(ctx.includes('INTERACTION AWARENESS'), `hard warning did not fire: ${ctx}`);
assert.ok(ctx.includes('Name these observations to the user and ask what they reflect'),
`hard warning omits the investigative framing: ${ctx}`);
// The required action is unchanged — this is a wording change, not a behaviour change.
assert.ok(ctx.includes('require you to suggest stopping'),
`hard warning dropped the required action: ${ctx}`);
});
it('leaves no verdict labels in the hook source', () => {
const src = readFileSync(
join(import.meta.dirname, '..', 'hooks', 'scripts', 'tool-tracker.mjs'), 'utf8');
for (const label of ['Rapid-fire', 'stuck/spiral', 'Consider a break']) {
assert.ok(!src.includes(label), `tool-tracker.mjs still contains verdict label: ${label}`);
}
});
});