Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
paths:
- 'docs/**'
- '.github/workflows/docs.yml'
pull_request:
paths:
- 'docs/**'
- '.github/workflows/docs.yml'
workflow_dispatch:

permissions:
Expand All @@ -18,35 +22,51 @@ concurrency:
cancel-in-progress: false

jobs:
build:
# PR validation: build the site to confirm it compiles, but don't deploy.
# Runs only on pull_request events.
validate-docs:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
working-directory: docs
- name: Build with Jekyll (validation only, no deploy)
run: bundle exec jekyll build --strict_front_matter --trace
working-directory: docs
env:
JEKYLL_ENV: production

# Production build for deploy. Runs only on push to main / workflow_dispatch.
build:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5

- name: Setup Ruby
uses: ruby/setup-ruby@v1
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
working-directory: docs

- name: Build with Jekyll
run: bundle exec jekyll build --verbose --trace --baseurl "${{ steps.pages.outputs.base_path }}"
working-directory: docs
env:
JEKYLL_ENV: production

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/_site

deploy:
needs: build
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
environment:
name: github-pages
Expand Down
78 changes: 78 additions & 0 deletions docs/concepts/hooks-deferred.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: Why hooks are deferred to v2
layout: default
parent: Concepts
nav_order: 5
---

= Why `hooks/` is deferred to v2

The repo lists `hooks/` as a category, but v1 ships zero hook files and the validator refuses any PR that adds a file under `hooks/` other than `.gitkeep`. This is deliberate. The gating rationale is in this page; the operational policy is in https://github.com/riboseinc/claude-memory-files/blob/main/SAFETY.md[`SAFETY.md`].

== The CVE history

Shared repos that ship hooks are a real supply-chain attack surface. The concrete precedent is **CVE-2025-59536** and **CVE-2026-21852** (Check Point Research, 2026):

* Hooks defined in repo-controlled `.claude/settings.json` ran **automatically on session start**, *before* the trust dialog.
* This allowed arbitrary shell execution plus `ANTHROPIC_BASE_URL` override, leading to API key exfiltration:

[quote, Check Point Research]
____
When a victim clones the repository and runs `claude`, their API key would be sent directly to the attacker's server — before the victim decides to trust the directory.
____

Anthropic patched by deferring network calls until trust, hardening the trust dialog, and gating MCP. The class of risk doesn't disappear — it just becomes harder to trigger. Any repo that ships hooks needs a review process commensurate with that risk class.

== Why v1's other categories are safe

The v1 categories (`instructions/`, `memory/`, `settings-fragments/`, `project-claude-md/`, `rules/`) carry **no executable payload**. Installing one of these:

* For markdown content (instructions, memory, project-claude-md, rules) → drops a file into `~/.claude/`. No shell runs.
* For settings-fragments → merges JSON into `~/.claude/settings.json`. No shell runs.

The blast radius of a malicious or sloppy PR in any of these categories is bounded to "Claude misbehaves in a particular way until the user removes the file." Annoying, recoverable, not exfiltrating credentials.

Hooks change that calculus. Even a well-intentioned hook with a hardcoded `/Users/<me>/...` path is broken-by-default for everyone else; a malicious one is straight RCE.

== The v2 policy (when `hooks/` opens)

When `hooks/` opens for submissions, every entry must:

. **Declare a mandatory `safety:` frontmatter field:**
+
[source,yaml]
----
safety:
runs-shell: true
network: false
modifies-files: true
reads-credentials: false
paths-touched: ["${CLAUDE_PLUGIN_ROOT}/**"]
----
+
Reviewers audit the file against this declaration. The validator enforces the field is populated with all sub-keys.

. **Pass a CI lint that rejects:**
* Absolute paths (`/Users/...`, `/home/...`).
* `$HOME` literals.
* `curl | bash` patterns (any unauthenticated remote-code-execution).
* Unsigned `curl` to non-allowlisted domains.
* Writes outside `${CLAUDE_PLUGIN_ROOT}`, `${CLAUDE_PLUGIN_DATA}`, or `~/.claude/`.

. **Get two-reviewer approval via CODEOWNERS.** Any PR touching `hooks/` requires the original author/owner **plus** the maintainer. Configured via branch protection rules referencing the auto-generated `.github/CODEOWNERS`.

. **Ship as a plugin** (recommended, not enforced) so execution is sandboxed under `${CLAUDE_PLUGIN_ROOT}` rather than reaching arbitrary filesystem state. Anthropic's plugin spec uses `${CLAUDE_PLUGIN_ROOT}` and `${CLAUDE_PLUGIN_DATA}` precisely for hook portability.

== Gate condition for v2 opening

`hooks/` opens for contributions after **at least three v1 PRs have flowed through the schema and the local validator has had real exercise.** The point is to learn what falls out of the schema before adding the highest-risk category. If the frontmatter contract or validator turns out to be wrong, it's safer to discover that on memory/instruction files than on hooks.

== In the meantime

The v1 validator rejects any file added under `hooks/` other than `.gitkeep`. This is enforced both locally (run `node _meta/validate.mjs`) and in CI (the workflow runs the validator on every PR). The empty `hooks/` directory exists only so that the schema and installer code paths can be smoke-tested against the rejection case.

== See also

* https://github.com/riboseinc/claude-memory-files/blob/main/SAFETY.md[`SAFETY.md`] — the full v2 hook policy.
* https://github.com/riboseinc/claude-memory-files/issues/1#issuecomment-4458965619[Issue #1 comment 3] — the v1/v2 split rationale.
* https://research.checkpoint.com/2026/rce-and-api-token-exfiltration-through-claude-code-project-files-cve-2025-59536/[Check Point Research writeup] — the CVE details.
18 changes: 18 additions & 0 deletions docs/concepts/index.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: Concepts
layout: default
nav_order: 3
has_children: true
---

= Concepts

Background pages explaining the design decisions that shape this repo. Read these to understand *why* the schema and category structure look the way they do, before you start submitting your own files.

Pages in this section:

* link:scope-rubric/[**Scope rubric**] — when to use `universal`, `team`, or `personal-share`, with worked examples from the seed.
* link:mechanism-and-data/[**Mechanism and data**] — the `requires-companion` pattern, demonstrated by `github-narrative-location` + `user-maintained-gems`.
* link:instructions-vs-memory/[**Instructions vs memory**] — the operational distinction between auto-loading instruction files and reference-only memory files.
* link:settings-fragments/[**Settings-fragments**] — how JSON snippets deep-merge into your `~/.claude/settings.json`, preserving hand-curated entries.
* link:hooks-deferred/[**Why hooks are deferred to v2**] — the CVE history that motivates the staged rollout.
66 changes: 66 additions & 0 deletions docs/concepts/instructions-vs-memory.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Instructions vs memory
layout: default
parent: Concepts
nav_order: 3
---

= Instructions vs memory: the operational distinction

Two of the v1 categories — `instructions/` and `memory/` — install to almost the same place under `~/.claude/`, but they behave very differently at session-load time. Picking the right one matters for both context-token cost and for how reliably the rule fires.

== The mechanical difference

[cols="1,2,2"]
|===
| Aspect | `instructions/` | `memory/`

| Install target
| `~/.claude/instructions/<name>.md`
| `~/.claude/memory/<name>.md`

| `@`-include in `CLAUDE.md`?
| **Yes** — installer appends `@instructions/<name>.md` under a managed-block marker.
| No.

| Loaded every session?
| **Yes** — counts toward each session's context budget.
| Only via the auto-memory `MEMORY.md` index, or when an instruction file references it inline.

| Frontmatter `autoload`
| Forced `true`.
| Forced `false`.

| Typical content
| Operational rules: "when X, do Y."
| Reference data, feedback narratives, incident history, supporting context for an instruction.
|===

== When to use which

**Use `instructions/`** when the rule needs to be present in every session before any work begins. Examples in the seed: `github-pr-title-issue-link` (PR titles must embed the issue URL), `narrate-plan` (state intent before each tool call), `sibling-clones` (clone to sibling dirs, not `/tmp`). These need to fire reliably or they don't fire at all.

**Use `memory/`** when the content is a reference the assistant *consults*, not a rule it *always follows*. Examples in the seed: `feedback_pr_always_assigned` (PR assignment discipline — the operational rule is in the paired instruction file `github-pr-assignment`; this memory file holds the supporting narrative and incident history), `feedback_external_docs_framing` (docs-writing guidelines — only relevant when authoring external docs), `user-maintained-gems` (the data table consumed by two instruction files).

A useful test: would a fresh Claude Code session need to load this file unconditionally to behave correctly? If yes, `instructions/`. If it can wait until the relevant work surfaces, `memory/`.

== Token-cost considerations

Every `@`-included instruction file costs context tokens in every session, whether the rule fires that session or not. The Anthropic guidance is to keep `CLAUDE.md` (and by extension everything @-included into it) under ~200 lines per file, and to consolidate where possible.

This is why the v1 seed only marks a handful of files as instructions. The seed has 11 instructions vs. 7 memory files. As contributors add more rules, the discipline is: prefer `memory/` unless the rule must fire every session.

== Instructions can reference memory

The cleanest pattern is an instruction file that **points at** a memory file rather than inlining its content. Worked example in v1:

* `instructions/github-narrative-location.md` — the operational rule. Universal-scope. Loaded every session.
* `memory/user-maintained-gems.md` — the maintainer table data. Personal-share. Not auto-loaded; the instruction's body says "consult your installed `user-maintained-gems.md`".

This keeps the always-loaded part short while putting the bulk content (potentially large tables, glossaries, incident histories) in memory files that load on demand. See link:../mechanism-and-data/[Mechanism and data] for the full pattern.

== See also

* link:../scope-rubric/[Scope rubric] — both categories use the same scope values.
* https://github.com/riboseinc/claude-memory-files/blob/main/SCHEMA.md[`SCHEMA.md`] — the full frontmatter contract for both types.
* https://code.claude.com/docs/en/memory[Anthropic's memory documentation] — authoritative on `CLAUDE.md`, `@`-imports, and the `MEMORY.md` cap.
63 changes: 63 additions & 0 deletions docs/concepts/mechanism-and-data.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: Mechanism and data
layout: default
parent: Concepts
nav_order: 2
---

= Mechanism and data: the `requires-companion` pattern

Some rules are *universal mechanisms* that depend on *personal data* to be operational. The `requires-companion:` frontmatter field is how the repo expresses that pairing: ship the mechanism as `scope: universal`, ship the data as `scope: personal-share`, and declare the dependency so the installer can warn when the companion isn't installed.

== The seed example

The clearest worked pair in the v1 seed:

[cols="1,3"]
|===
| `instructions/github-narrative-location.md`
| **Mechanism (universal).** "For gems you maintain, the detailed change narrative belongs in **issue/ticket comments**; for gems maintained by other people, it belongs in the **PR description**." This rule is universal — every developer working across owned and non-owned gems benefits from it. Declares `requires-companion: [user-maintained-gems]`.

| `memory/user-maintained-gems.md`
| **Data (personal-share, skeletonised).** The maintainer table that says *which* gems each user owns and who the default maintainer is for the rest. Nick's metanorma row ships as a worked example; every other user rewrites it for their own setup. The validator strictly enforces single-owner (`owners == [author.github]`) for personal-share files.
|===

A second pair uses the same data file from a different angle: `instructions/github-pr-assignment.md` (mechanism: every PR has an assignee, owned vs non-owned routing) also declares `requires-companion: [user-maintained-gems]`. One data file, two consuming mechanisms.

== Why the pattern matters

Without `requires-companion`, two failure modes loom:

1. **Mechanism alone, no data.** `github-narrative-location` is universal in principle but inert in practice — it tells Claude "consult your installed `user-maintained-gems.md`" and trusts that file exists. If a user installs only the instruction and not the data, the rule has nothing to consult. The installer surfaces this with a `requires-companion` warning at install time.

2. **Mechanism with data inlined.** Naively, the maintainer table could live inside the instruction file itself. But that makes the file personal — `scope: universal` would be a lie, and every contributor would have to override the table on install, defeating the cherry-pick model.

The pattern keeps the data factored out, declared as personal, and installed separately. The validator enforces the integrity (`personal-share` ⇒ single-owner), and the installer reports any missing companion at install time.

== Behaviour on install

When you install a file with `requires-companion`, the installer checks whether the companion slug is present:

* If the companion is already installed → silent install.
* If the companion is in the repo but not yet installed locally → the installer prints a warning suggesting you install it too.
* If the companion isn't in the repo at all → the validator already caught that at PR time (with a warning on the dependent file).

[source,bash]
----
$ ./tools/install.sh instructions/github-narrative-location.md
Installing github-narrative-location (type: instruction, scope: universal)
Note: requires-companion: user-maintained-gems is not yet installed.
Consider: ./tools/install.sh memory/user-maintained-gems.md
----

(The actual warning text may differ; the principle is the same.)

== When you'd use this pattern in a new file

If you're drafting a rule and find yourself writing "consult your local list of X" or "see your team's table of Y", you're probably looking at a mechanism/data split. Factor the data into a separate file under `memory/` with `scope: personal-share` and `owners: [your-handle]`; declare `requires-companion: [<data-slug>]` in the mechanism.

== See also

* link:../scope-rubric/[Scope rubric] — when to use `universal` vs `team` vs `personal-share`.
* https://github.com/riboseinc/claude-memory-files/blob/main/SCHEMA.md[`SCHEMA.md`] — `requires-companion:` field definition.
* https://github.com/riboseinc/claude-memory-files/issues/1#issuecomment-4458965838[Issue #1 comment 5] — the seed-set discussion, where the `github-narrative-location` ↔ `user-maintained-gems` pair was first laid out.
75 changes: 75 additions & 0 deletions docs/concepts/scope-rubric.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: Scope rubric
layout: default
parent: Concepts
nav_order: 1
---

= Scope rubric: universal, team, personal-share

Every content file declares a `scope:` field. Picking the right one is the most important triage decision for a contribution; it determines who reviews the PR, who installs the file, and how the picker filters it.

[NOTE]
====
The `scope` field has no precedent in any other Claude Code aggregator. It's the explicit answer to the failure mode catalogued in cursor-rules: most rules in the wild are one person's idiom framed as universal, with no convention separating "my taste" from "team practice" from "ecosystem fact".
====

== `universal`

Applies regardless of team or org context. The rule is generic enough that any Claude Code user would benefit from installing it.

**Examples in the seed:**

* `github-pr-title-issue-link` — embed the full issue URL in PR titles so GitHub renders the cross-reference card properly. A convention about how a third-party service (GitHub) renders backlinks; nothing org-specific.
* `paragraph-shape` — soft max 4 sentences per paragraph in narrow-side-panel narration. A UX discipline about narration shape, not tied to any team's workflow.
* `sibling-clones` — clone external repos into the parent of `cwd`, not `/tmp`. A workflow preference about persistence across sessions.

**Rule of thumb:** if you can explain the rule without naming a team, an org, or a specific project, it's probably universal.

== `team`

A team's operating norm. Two sub-cases:

**Generic team practice** — any team could adopt this norm. Examples: `feedback_pr_always_assigned` (every PR has an assignee at creation), `feedback_pr_chain_no_local_deps` (cross-repo Ruby PR sequences use `Gemfile.devel` git+branch refs, not local overrides). Different teams may have different specifics, but the *shape* of the rule applies broadly.

**Specific-team norm** — the rule only makes sense for a named team. Examples: `github-no-releases` (metanorma's fortnightly-release cycle) and `github-metanorma-project` (auto-add issues to org project #15) both apply only to `team: metanorma`. Use the optional `team:` qualifier so the picker can filter:

[source,bash]
----
/list-memory-files --team metanorma
----

**Rule of thumb:** if the rule names a team in its body, declare `team: <name>` in the frontmatter. Otherwise leave it off.

== `personal-share`

A single author's data or preference, shared for opt-in cherry-pick. **Not** endorsed as team norm.

The validator strictly enforces `owners == [author.github]` (length 1, exact author) for personal-share entries. Third parties who want to amend fork as a new slug (e.g. `andrew-maintained-gems` parallel to `user-maintained-gems`); they don't co-own the original author's file.

Personal-share covers two flavours:

* **Data templates**, like `user-maintained-gems.md` — the maintainer table is a worked example you customise with your own gem list. "Your data goes here; here's how Nick filled it in."
* **Behavioural preferences**, like `feedback_handhold_when_frustrated.md` — the recovery template is the universal part; the specific distress signals (swearing, ALL CAPS, repeated "again") are Nick's tells. Other users adjust to their own.

**Rule of thumb:** if the rule names "I" or "my", or if every user needs to fork-and-customise the contents rather than installing verbatim, it's `personal-share`.

== Decision flow

[mermaid]
....
flowchart TD
A[Drafting a new file] --> B{Does the rule name<br/>a specific team/org?}
B -->|Yes| C[scope: team<br/>team: name]
B -->|No| D{Is every user expected<br/>to customise the contents<br/>before using it?}
D -->|Yes| E[scope: personal-share<br/>owners: single author]
D -->|No| F{Is the rule's principle<br/>tied to a workflow shared<br/>by many teams?}
F -->|Yes| G[scope: team<br/>no team: qualifier]
F -->|No| H[scope: universal]
....

== See also

* link:../mechanism-and-data/[Mechanism and data] — how the `requires-companion` pattern lets a `universal` instruction pair with a `personal-share` data file.
* https://github.com/riboseinc/claude-memory-files/blob/main/SCHEMA.md[`SCHEMA.md`] — the full frontmatter contract.
* https://github.com/riboseinc/claude-memory-files/issues/1#issuecomment-4458965505[Issue #1 comment 2] — the design rationale for adding `scope:` to the schema.
Loading
Loading