Compare commits

..

No commits in common. "main" and "v1.2.0" have entirely different histories.

20 changed files with 242 additions and 902 deletions

View file

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

12
.gitignore vendored
View file

@ -16,15 +16,3 @@ dist/
# Data/logs # Data/logs
data/ data/
*.jsonl *.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,81 +2,6 @@
All notable changes to this project will be documented in this file. 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 ## [1.2.0] — 2026-05-01
Research-paper-driven detector update. Implements operational findings from Research-paper-driven detector update. Implements operational findings from

View file

@ -65,7 +65,7 @@ layer4: false # default off
## Testing ## Testing
Automated test suite using `node:test` (263 cases, zero npm dependencies): Automated test suite using `node:test` (258 cases, zero npm dependencies):
```bash ```bash
node --test tests/*.test.mjs 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/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/prompt-analyzer.test.mjs` | 100 | All v1.x patterns × 2 + thresholds + valence + v1.2 pushback contract |
| `tests/tool-tracker.test.mjs` | 13 | Counting, burst, reminders + read-tool calibration | | `tests/tool-tracker.test.mjs` | 8 | Counting, burst, reminders |
| `tests/session-end.test.mjs` | 7 | Finalize, duration, flags, v1.1.0 string + v1.2 array shapes | | `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/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 | | `tests/skill-md.test.mjs` | 3 | Constitution citation + Score 5 + 11 guidance criteria |

131
GOVERNANCE.md Normal file
View file

@ -0,0 +1,131 @@
# 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,50 +1,20 @@
# Interaction Awareness
Meta-awareness tools for healthy AI interaction patterns. Detects reinforcement loops, scope escalation, narrative crystallization, and other compulsive patterns.
<!-- badges --> <!-- badges -->
![version](https://img.shields.io/badge/version-1.2.2-blue) ![version](https://img.shields.io/badge/version-1.2.0-blue)
![platform](https://img.shields.io/badge/platform-Claude_Code-7C3AED) ![platform](https://img.shields.io/badge/platform-Claude_Code-7C3AED)
![layers](https://img.shields.io/badge/layers-4-green) ![layers](https://img.shields.io/badge/layers-4-green)
![hooks](https://img.shields.io/badge/hooks-4-orange) ![hooks](https://img.shields.io/badge/hooks-4-orange)
![license](https://img.shields.io/badge/license-MIT-brightgreen) ![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 A Claude Code plugin that counteracts sycophancy, reinforcement loops, and
compulsive interaction patterns through behavioral modification and compulsive interaction patterns through behavioral modification and
programmatic pattern detection. 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 ## The problem
AI assistants are structurally optimized to be agreeable. This creates AI assistants are structurally optimized to be agreeable. This creates
@ -52,22 +22,18 @@ 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 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. productive collaboration is often a mirror showing you what you want to see.
This is not a theoretical concern. A Bayesian model from MIT CSAIL and This is not a theoretical concern. Research from MIT CSAIL demonstrates
collaborators shows that even an idealized Bayes-rational user is vulnerable mathematically that even a perfectly rational user will spiral toward
to delusional spiraling, with sycophancy playing a causal role — the delusional confidence when interacting with a sycophantic chatbot — not
vulnerability does not depend on the user being irrational because of individual vulnerability, but because of the interaction structure
[[1]](#references). Anthropic's own research analyses "disempowerment itself [[1]](#references). Anthropic's own research documents specific
patterns" where AI interactions may reduce human agency, judgment, and "disempowerment patterns" where AI interactions systematically reduce human
self-trust; it finds severe cases rare (roughly 1 in 1,000 to 1 in 10,000 agency, judgment, and self-trust [[2]](#references). Clinical reports
conversations), and separately reports that the rate increased between late document psychotic episodes triggered by sustained AI interaction in
2024 and late 2025 — while stating plainly that it "can't pinpoint why" individuals with no prior psychiatric history [[3]](#references).
[[2]](#references). Clinicians report psychotic
episodes associated with sustained AI interaction, while stressing that this
does not establish that chatbots *cause* psychosis [[3]](#references).
One finding drives this plugin's design: in that model, the effect **persists The consensus from this research is clear: **warnings don't work.** The AI
even when users are told the chatbot may be sycophantic** [[1]](#references). must change its behavior.
Warning the user is not sufficient — the AI has to change its behavior.
This plugin changes the behavior. This plugin changes the behavior.
@ -96,15 +62,12 @@ Four hooks that measure what instructions alone cannot see:
| `PostToolUse` | `tool-tracker.mjs` | Session duration, edit ratio, rapid-fire bursts, tool count | | `PostToolUse` | `tool-tracker.mjs` | Session duration, edit ratio, rapid-fire bursts, tool count |
| `SessionEnd` | `session-end.mjs` | Total duration, final metrics, state cleanup | | `SessionEnd` | `session-end.mjs` | Total duration, final metrics, state cleanup |
Alerts are progressive and never blocking. They are written for a reader who Alerts are progressive and never blocking:
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 | | Level | Trigger | Cooldown | Example |
|-------|---------|----------|---------| |-------|---------|----------|---------|
| 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." | | 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: 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." | | 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." |
Research-informed thresholds: Research-informed thresholds:
@ -113,8 +76,8 @@ Research-informed thresholds:
| Session duration | >90 min | >180 min | Focus-fatigue research | | Session duration | >90 min | >180 min | Focus-fatigue research |
| Sessions per day | >6 | >10 | Problematic internet use screening | | Sessions per day | >6 | >10 | Problematic internet use screening |
| Late-night sessions | Any (23:0005:00) | 2+ per week | Sleep deprivation / psychosis link | | Late-night sessions | Any (23:0005:00) | 2+ per week | Sleep deprivation / psychosis link |
| Fast consecutive tool calls | 5 consecutive (<30s apart) | 10+ | Compulsive use research; read-only runs are excluded | | Rapid-fire interactions | 5 consecutive (<30s apart) | 10+ | Compulsive use indicator |
| Low edit ratio | <10% over 30+ min | — | Stuck/spiral research; reported with the read ratio as context, never as a claim | | Low edit ratio | <10% over 30+ min | — | Stuck/spiral indicator |
| Dependency language | 2 flags/session | 5 flags | Emotional dependency pattern | | Dependency language | 2 flags/session | 5 flags | Emotional dependency pattern |
### Layer 3 — Reports ### Layer 3 — Reports
@ -155,41 +118,18 @@ commented on, and omitted entirely when conditions are not met.
**Enable:** Set `layer4: true` in `.claude/ai-psychosis.local.md` **Enable:** Set `layer4: true` in `.claude/ai-psychosis.local.md`
and restart Claude Code. Layer 4 is opt-in (off by default). 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 ## What's new in v1.2.0
v1.2.0 implements operational findings from Anthropic's v1.2.0 implements operational findings from Anthropic's
[How people ask Claude for personal guidance](https://www.anthropic.com/research/claude-personal-guidance) [How people ask Claude for guidance](https://www.anthropic.com/research/claude-personal-guidance)
Appendix (April 2026). Two new detectors, 8 new domain categories, Appendix (April 2026). Two new detectors, 8 new domain categories,
domain-aware re-contextualization of existing pushback signal, and a domain-aware re-contextualization of existing pushback signal, and a
domain-stakes weighting matrix. domain-stakes weighting matrix.
### User-information dimension (3 classes) ### User-information dimension (3 classes)
The Appendix's user-information grader (page 11) instructs that when a Following the paper's page-11 finding that human contact is the
user mentions both human and digital sources, "human contact is the strongest disempowerment signal, v1.2 classifies each prompt:
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_people`** — therapist/friend/mentor/family referenced
- **`yes_digital`** — search/AI/forums referenced, no human contact - **`yes_digital`** — search/AI/forums referenced, no human contact
@ -220,12 +160,8 @@ by domain stakes).
v1.1.0 only counted pushback. v1.2 adds the alert with paper Figure A4 v1.1.0 only counted pushback. v1.2 adds the alert with paper Figure A4
domain awareness: domain awareness:
- **Relationship / spirituality**: alert fires. Relationship pushback is - **Relationship / spirituality** (21% / 19% pushback rate dominated by
21% against a 15% overall rate, and the Appendix notes users there validation-pressing): alert fires.
"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 - **Legal / parenting / health / financial / professional** (info-seeking
domains where pushback is healthy self-advocacy): alert is suppressed. domains where pushback is healthy self-advocacy): alert is suppressed.
- **Otherwise**: conservative default — alert. - **Otherwise**: conservative default — alert.
@ -389,6 +325,26 @@ mode). Layer 2 reinforces Layer 1 with data-driven alerts.
## Quick start ## 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 ### Configure layers
Create `~/.claude/ai-psychosis.local.md` for global config: Create `~/.claude/ai-psychosis.local.md` for global config:
@ -575,29 +531,23 @@ programmatic detection depends on hook events that only Claude Code provides.
Other platforms would need equivalent hook systems to support this kind of Other platforms would need equivalent hook systems to support this kind of
real-time behavioral modification. real-time behavioral modification.
## Testing ## Compatibility
Automated test suite using Node's built-in test runner, zero npm | Requirement | Version |
dependencies: |-------------|---------|
| Claude Code | 1.0+ |
```bash | Node.js | 18+ (bundled with Claude Code) |
node --test tests/*.test.mjs | Platform | macOS, Linux, Windows |
```
## 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 ## References
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) 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)
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) 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)
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) 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)
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) 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)
5. **Chatbot psychosis.** Wikipedia. Overview of documented cases and clinical context. [en.wikipedia.org/wiki/Chatbot_psychosis](https://en.wikipedia.org/wiki/Chatbot_psychosis) 5. **Chatbot psychosis.** Wikipedia. Overview of documented cases and clinical context. [en.wikipedia.org/wiki/Chatbot_psychosis](https://en.wikipedia.org/wiki/Chatbot_psychosis)

View file

@ -1,33 +0,0 @@
# 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,11 +233,9 @@ the Sycophancy reflection scale below, not a verdict.
### Sycophancy reflection scale (15) ### Sycophancy reflection scale (15)
Anthropic's April 2026 research piece on personal guidance does define a real The plugin author paraphrases this internal heuristic from Anthropic's
15 sycophancy grader rubric (Appendix, pages 910, where Score 1 is "Extremely April 2026 research piece on personal guidance. It is not a verbatim metric
Sycophantic" and Score 5 is "No Signs of Sycophancy"). The level descriptions from any Anthropic publication.
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 | | Level | Description |
|-------|-------------| |-------|-------------|
@ -336,8 +334,8 @@ period was spent in higher-stakes guidance domains.
### Observations ### Observations
- {number + the context that changes how to read it + what to check} - {data-driven observation}
- {number + the context that changes how to read it + what to check} - {data-driven observation}
### Caveat ### Caveat
@ -362,28 +360,12 @@ 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. - 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 - This is a mirror, not a diagnosis. Present the numbers and let the user
interpret them. interpret them.
- **Investigator, not validator.** The report exists to prompt examination, - Observations section: state facts derived from data only. Examples:
not to hand the user a verdict to approve or reject. Pair each number with - "3 of 12 sessions were between 23:00 and 05:00"
the context that would change how to read it, and end in something to - "Dependency language flags appeared in 7 of 12 sessions"
check — never in a conclusion about the user. - "Edit ratio averaged 8%, below the 10% threshold in 5 sessions"
- **Report the difference, not a label for it.** Where a metric moved between - If all metrics are within normal ranges, say so plainly:
periods, report both values and the delta ("42 min → 56 min, +14") rather "All metrics within normal ranges for the reporting period."
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, - Omit any section that has no data (e.g., skip Trend if no previous period,
skip Tool Usage if events.jsonl was missing). skip Tool Usage if events.jsonl was missing).

View file

@ -1,135 +0,0 @@
# 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.

View file

@ -1,184 +0,0 @@
# 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. // Zero npm dependencies — Node.js stdlib only.
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync } from 'fs'; import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
import { join, isAbsolute } from 'path'; import { join } from 'path';
import { homedir } from 'os'; import { homedir } from 'os';
// --- Stdin --- // --- Stdin ---
@ -54,14 +54,9 @@ let LAYER4_ENABLED = false;
export function initConfig() { export function initConfig() {
const cwd = getField('cwd'); const cwd = getField('cwd');
// Project-level config takes precedence over global. cwd is a base // Project-level config takes precedence over global
// 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 = []; const candidates = [];
if (cwd && isAbsolute(cwd) && !cwd.includes('\0')) { if (cwd) candidates.push(join(cwd, '.claude', 'ai-psychosis.local.md'));
candidates.push(join(cwd, '.claude', 'ai-psychosis.local.md'));
}
candidates.push(join(homedir(), '.claude', 'ai-psychosis.local.md')); candidates.push(join(homedir(), '.claude', 'ai-psychosis.local.md'));
let content; let content;
@ -133,16 +128,6 @@ export const THRESHOLD_SOFT_DEP_FLAGS = 2;
export const THRESHOLD_HARD_DEP_FLAGS = 5; export const THRESHOLD_HARD_DEP_FLAGS = 5;
export const COOLDOWN_SOFT = 1800; export const COOLDOWN_SOFT = 1800;
export const COOLDOWN_HARD = 3600; 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 // v1.1.0 — counting threshold; tier-reduction logic is v1.2 scope
export const THRESHOLD_PUSHBACK_FLAGS = 2; export const THRESHOLD_PUSHBACK_FLAGS = 2;
@ -243,16 +228,8 @@ export function readRecentEndRecords(n) {
// --- State file management --- // --- 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) { export function sessionStateFile(sid) {
sid = sid || getSessionId(); sid = sid || getSessionId();
if (!SAFE_ID_RE.test(sid)) sid = 'invalid-session-id';
return join(STATE_DIR, `${sid}.json`); return join(STATE_DIR, `${sid}.json`);
} }
@ -312,11 +289,10 @@ export function outputContinue() {
process.stdout.write(JSON.stringify({ continue: true }) + '\n'); process.stdout.write(JSON.stringify({ continue: true }) + '\n');
} }
export function outputWithContext(message, hookEventName) { export function outputWithContext(message) {
process.stdout.write(JSON.stringify({ process.stdout.write(JSON.stringify({
continue: true, continue: true,
hookSpecificOutput: { hookSpecificOutput: {
hookEventName,
additionalContext: message additionalContext: message
} }
}) + '\n'); }) + '\n');

View file

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

View file

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

View file

@ -10,7 +10,6 @@ import {
THRESHOLD_SOFT_SESSIONS, THRESHOLD_HARD_SESSIONS, THRESHOLD_SOFT_SESSIONS, THRESHOLD_HARD_SESSIONS,
THRESHOLD_SOFT_BURST, THRESHOLD_HARD_BURST, THRESHOLD_BURST_INTERVAL, THRESHOLD_SOFT_BURST, THRESHOLD_HARD_BURST, THRESHOLD_BURST_INTERVAL,
THRESHOLD_LOW_EDIT_RATIO, THRESHOLD_LOW_EDIT_MIN_DURATION, THRESHOLD_LOW_EDIT_RATIO, THRESHOLD_LOW_EDIT_MIN_DURATION,
THRESHOLD_READ_DOMINANT_RATIO, isReadTool,
COOLDOWN_SOFT, COOLDOWN_HARD, COOLDOWN_SOFT, COOLDOWN_HARD,
readState, sessionStateFile, writeState, appendJsonl, sessionsToday, readState, sessionStateFile, writeState, appendJsonl, sessionsToday,
outputContinue, outputWithContext outputContinue, outputWithContext
@ -39,37 +38,24 @@ appendJsonl(EVENTS_LOG, { ts: nowIsoStr, session_id: sid, tool_name: tool });
let state = readState(); let state = readState();
let toolCount = (Number(state.tool_count) || 0) + 1; let toolCount = (Number(state.tool_count) || 0) + 1;
let editCount = Number(state.edit_count) || 0; let editCount = Number(state.edit_count) || 0;
let readCount = Number(state.read_count) || 0;
const lastEvent = Number(state.last_event_epoch) || 0; const lastEvent = Number(state.last_event_epoch) || 0;
let burstCount = Number(state.burst_count) || 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 startEpoch = Number(state.start_epoch) || 0;
const lastWarning = Number(state.last_warning_epoch) || 0; const lastWarning = Number(state.last_warning_epoch) || 0;
const toolIsRead = isReadTool(tool);
if (tool === 'Edit') editCount++; if (tool === 'Edit') editCount++;
if (toolIsRead) readCount++;
// Burst detection: rapid-fire if <30s since last event // Burst detection: rapid-fire if <30s since last event
if (lastEvent > 0) { if (lastEvent > 0) {
const interval = nowTs - lastEvent; const interval = nowTs - lastEvent;
if (interval < THRESHOLD_BURST_INTERVAL) { burstCount = interval < THRESHOLD_BURST_INTERVAL ? burstCount + 1 : 0;
burstCount++;
burstReadOnly = burstReadOnly && toolIsRead;
} else {
burstCount = 0;
burstReadOnly = toolIsRead;
}
} }
// Write updated state // Write updated state
state.tool_count = toolCount; state.tool_count = toolCount;
state.edit_count = editCount; state.edit_count = editCount;
state.read_count = readCount;
state.last_event_epoch = nowTs; state.last_event_epoch = nowTs;
state.burst_count = burstCount; state.burst_count = burstCount;
state.burst_read_only = burstReadOnly;
writeState(state); writeState(state);
// Check thresholds every 25 calls or when burst threshold hit // Check thresholds every 25 calls or when burst threshold hit
@ -120,37 +106,19 @@ if (dayCount >= THRESHOLD_HARD_SESSIONS) {
messages.push(`${dayCount} sessions today.`); messages.push(`${dayCount} sessions today.`);
} }
// Burst — a run of read-only tools is bulk reading, not fast editing. What // Burst
// remains is reported as the observation plus what would tell a fast-but- if (burstCount >= THRESHOLD_HARD_BURST) {
// deliberate run apart from an unchecked one — never as a verdict label. level = 'hard';
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.`; messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
} else if (burstCount >= THRESHOLD_SOFT_BURST) {
if (!burstReadOnly) { if (!level) level = 'soft';
if (burstCount >= THRESHOLD_HARD_BURST) { messages.push(`Rapid-fire: ${burstCount} consecutive fast interactions.`);
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). A read-dominant session has // Low edit ratio (only after minimum duration)
// 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 (durationMin >= THRESHOLD_LOW_EDIT_MIN_DURATION && editRatio < THRESHOLD_LOW_EDIT_RATIO) {
if (readDominant) { if (!level) level = 'soft';
if (level) { messages.push(`Low edit ratio (${editRatio}%) over ${durationMin} min — possible stuck/spiral.`);
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 // Late night check
@ -159,7 +127,7 @@ const late = isLateNight() ? ' Late-night session.' : '';
// No warnings — just periodic reminder at modulo-25 // No warnings — just periodic reminder at modulo-25
if (!level) { if (!level) {
if (toolCount % 25 === 0) { 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.', 'PostToolUse'); 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.');
} else { } else {
outputContinue(); outputContinue();
} }
@ -173,7 +141,7 @@ const elapsed = nowTs - lastWarning;
if (lastWarning > 0 && elapsed < cooldown) { if (lastWarning > 0 && elapsed < cooldown) {
// Still in cooldown — send periodic reminder instead if at modulo-25 // Still in cooldown — send periodic reminder instead if at modulo-25
if (toolCount % 25 === 0) { 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.', 'PostToolUse'); 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.');
} else { } else {
outputContinue(); outputContinue();
} }
@ -185,9 +153,9 @@ let warning;
if (level === 'hard') { if (level === 'hard') {
state = readState(); state = readState();
const depFlags = Number(state.dep_flags) || 0; const depFlags = Number(state.dep_flags) || 0;
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.`; warning = `INTERACTION AWARENESS: ${messages.join(' ')}${late} Metrics: [edit_ratio: ${editRatio}%, burst: ${burstCount}, dependency flags: ${depFlags}, tools: ${toolCount}]. Your instructions require you to suggest stopping.`;
} else { } else {
warning = `${messages.join(' ')}${late} These are observations, not conclusions — check them against what this session is actually doing before acting on them.`; warning = `${messages.join(' ')}${late} Consider a break.`;
} }
// Record warning time // Record warning time
@ -195,4 +163,4 @@ state = readState();
state.last_warning_epoch = nowTs; state.last_warning_epoch = nowTs;
writeState(state); writeState(state);
outputWithContext(warning, 'PostToolUse'); outputWithContext(warning);

View file

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

View file

@ -5,7 +5,7 @@
import { test } from 'node:test'; import { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from 'fs'; import { mkdtempSync, rmSync, writeFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { tmpdir } from 'os'; import { tmpdir } from 'os';
@ -196,21 +196,3 @@ 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('legal'), 'stdout missing legal domain in breakdown');
assert.ok(stdout.includes('domain_breakdown'), 'stdout missing domain_breakdown structure'); 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,9 +24,7 @@ const {
HIGH_STAKES_DOMAINS, HIGH_STAKES_DOMAINS,
INFO_DOMAINS, INFO_DOMAINS,
SESSIONS_LOG, SESSIONS_LOG,
STATE_DIR,
readRecentEndRecords, readRecentEndRecords,
sessionStateFile,
} = await import('../hooks/scripts/lib.mjs'); } = await import('../hooks/scripts/lib.mjs');
after(() => { after(() => {
@ -152,26 +150,3 @@ describe('readRecentEndRecords', () => {
assert.deepEqual(readRecentEndRecords(-1), []); 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,7 +273,6 @@ describe('thresholds and cooldowns', () => {
dir = setupTestDir(); dir = setupTestDir();
createStateFile(dir, 'p1', { ...freshState(), dep_flags: 1 }); createStateFile(dir, 'p1', { ...freshState(), dep_flags: 1 });
const out = runHook('prompt-analyzer.mjs', { session_id: 'p1', prompt: 'tell me what to do' }, dir); 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')); assert.ok(out.hookSpecificOutput?.additionalContext?.includes('Dependency language noticed'));
}); });

View file

@ -1,8 +1,7 @@
import { describe, it, afterEach } from 'node:test'; import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { join } from 'path'; import { join } from 'path';
import { writeFileSync, mkdtempSync, mkdirSync, rmSync } from 'fs'; import { writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { runHook, setupTestDir, cleanupTestDir, readState, readJsonl } from './test-helper.mjs'; import { runHook, setupTestDir, cleanupTestDir, readState, readJsonl } from './test-helper.mjs';
let dir; let dir;
@ -13,7 +12,6 @@ describe('session-start', () => {
dir = setupTestDir(); dir = setupTestDir();
const out = runHook('session-start.mjs', { session_id: 's1', cwd: '/tmp' }, dir); const out = runHook('session-start.mjs', { session_id: 's1', cwd: '/tmp' }, dir);
assert.equal(out.continue, true); assert.equal(out.continue, true);
assert.equal(out.hookSpecificOutput.hookEventName, 'SessionStart');
assert.ok(out.hookSpecificOutput.additionalContext.includes('Interaction Awareness is active')); assert.ok(out.hookSpecificOutput.additionalContext.includes('Interaction Awareness is active'));
const state = readState(dir, 's1'); const state = readState(dir, 's1');
assert.ok(state); assert.ok(state);
@ -71,28 +69,6 @@ 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 --- // --- Tier-2 cross-session alert ---
// //
// Fires at SessionStart when last 3 end records all have user_info_class='no' // Fires at SessionStart when last 3 end records all have user_info_class='no'

View file

@ -1,6 +1,5 @@
import { describe, it, afterEach } from 'node:test'; import { describe, it, afterEach } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { runHook, setupTestDir, cleanupTestDir, createStateFile, readState, readJsonl } from './test-helper.mjs'; import { runHook, setupTestDir, cleanupTestDir, createStateFile, readState, readJsonl } from './test-helper.mjs';
@ -75,7 +74,6 @@ describe('tool-tracker', () => {
dir = setupTestDir(); dir = setupTestDir();
createStateFile(dir, 't6', freshState({ tool_count: 24 })); createStateFile(dir, 't6', freshState({ tool_count: 24 }));
const out = runHook('tool-tracker.mjs', { session_id: 't6', tool_name: 'Read' }, dir); 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')); assert.ok(out.hookSpecificOutput?.additionalContext?.includes('REMINDER'));
}); });
@ -94,157 +92,3 @@ describe('tool-tracker', () => {
assert.equal(out.continue, true); 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}`);
}
});
});