workspace: version the root CLAUDE.md + InstructionsLoaded hook, and report which rules fire (R-229)
gates / gates (push) Successful in 8s
gates / gates (push) Successful in 8s
install_workspace.py lays down the two things that shaped every session while existing on one host only. Unlike install_skills.py the targets are LIVE CONFIG, so: timestamped backup before every write, settings.json MERGED (this script owns exactly one key), a diverged CLAUDE.md reported rather than silently resolved, and an unparseable settings.json refused outright. Proven: all 7 top-level settings keys survived byte-identically, and run 2 wrote nothing. rules_report.py surfaces the column that matters -- rules that have NEVER fired, which are mis-globbed or dead. 6 of 9 on first run. The hook now self-rotates at 5 MB. The memory store is BACKED UP, NOT COMMITTED (auto-written, may name hosts/paths): added to dooplex-backup.service's User Data component. /opt/backup/scripts/ is itself unversioned host state -- filed, not fixed here.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# `workspace/` — the parts of the workspace that live outside every repo
|
||||
|
||||
Claude Code runs from `/mnt/5_hdd/felhom.eu/git`, which is **not a git repository**. Three things
|
||||
that shape every session live there or in `~/.claude/` and are therefore on one machine only:
|
||||
|
||||
| Thing | Live path | Versioned where | Installed by |
|
||||
|---|---|---|---|
|
||||
| workspace-root instructions | `<workspace>/CLAUDE.md` | `documentation/runbooks/workspace-CLAUDE.md` | `scripts/install_workspace.py` |
|
||||
| `InstructionsLoaded` hook | `~/.claude/settings.json` | `workspace/hooks/instructions-loaded.json` | `scripts/install_workspace.py` |
|
||||
| auto-memory store | `<workspace>/.claude-memory/` | **not versioned — backed up** | `dooplex-backup.service` |
|
||||
|
||||
## Why the CLAUDE.md source is NOT in this directory
|
||||
|
||||
`documentation/runbooks/workspace-CLAUDE.md` is **canonical** and stays where it is. It already has a
|
||||
gate enforcing byte-identity with the live file (`instructions_gate.py` check 5), and it is the path
|
||||
every existing pointer names. Copying it here would create a *third* copy of a file whose whole
|
||||
problem is that copies drift — the installer reads the canonical path instead.
|
||||
|
||||
## Why the memory store is backed up and not committed
|
||||
|
||||
It is **auto-written**: Claude writes it, so nobody reviews it before it lands. It may name hosts,
|
||||
paths and out-of-band secret locations that this project's secrets rule keeps out of committed
|
||||
files. A scan on 2026-08-06 found no credential *values* (535 keyword mentions across 104 files, one
|
||||
`key: value`-shaped hit that was prose, zero private-key blocks) — but "no secrets today" is not a
|
||||
property a directory keeps on its own when a machine writes to it unattended.
|
||||
|
||||
So it is protected by `dooplex-backup.service`'s User Data component instead
|
||||
(`CLAUDE_MEMORY_DIR` in `/opt/backup/scripts/backup-config.sh`).
|
||||
|
||||
The exact change, recorded here because the file it was made in is **not** version-controlled:
|
||||
|
||||
```sh
|
||||
# /opt/backup/scripts/backup-config.sh
|
||||
export CLAUDE_MEMORY_DIR="/mnt/5_hdd/felhom.eu/git/.claude-memory"
|
||||
|
||||
# /opt/backup/scripts/backup-data.sh — restic takes multiple paths; an absent optional path is
|
||||
# skipped with a printed reason, never a failure
|
||||
local backup_paths=("${DATA_SOURCE_DIR}")
|
||||
if [ -n "${CLAUDE_MEMORY_DIR:-}" ] && [ -d "${CLAUDE_MEMORY_DIR}" ]; then
|
||||
backup_paths+=("${CLAUDE_MEMORY_DIR}")
|
||||
fi
|
||||
restic -r "${RESTIC_REPO_DATA}" backup "${backup_paths[@]}" "${exclude_args[@]}" ...
|
||||
```
|
||||
|
||||
**`/opt/backup/scripts/` is itself unversioned host state on DooPlex** — no repo tracks it. That is a
|
||||
fresh instance of the very class this directory exists to close, found while closing it. Filed as a
|
||||
register item rather than fixed here: bringing a root-owned production backup script under version
|
||||
control (and deciding what installs it) is its own change, not a rider on this one.
|
||||
|
||||
**One-time cost of the change, so it is not mistaken for a fault:** adding a path to a restic path
|
||||
set invalidates the parent-snapshot match, so the first run after this edit logs
|
||||
`no parent snapshot found, will read all files` and re-reads the whole source (405 GiB). Chunk-level
|
||||
dedup means storage barely moves; subsequent runs find a parent and are incremental again.
|
||||
|
||||
**Caveat, stated because it is easy to misread as safety:** that backup's destination
|
||||
(`/mnt/5_hdd/backup`) is on the **same physical disk** as the store. It protects against accidental
|
||||
deletion, **not** against loss of `sda1`, and there is no off-site leg for the DooPlex backup set.
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"_comment": [
|
||||
"InstructionsLoaded hook — logs which instruction file loaded, when, and WHY.",
|
||||
"Installed into ~/.claude/settings.json by scripts/install_workspace.py (merged, never replaced).",
|
||||
"load_reason is the field that answers 'why': session_start | nested_traversal |",
|
||||
"path_glob_match | include | compact. trigger_file_path names the file whose read caused it.",
|
||||
"The hook is observability-only; Claude Code does not let it block.",
|
||||
"Self-rotating at 5 MB, one generation kept, because the log is unbounded otherwise.",
|
||||
"Read it with scripts/rules_report.py — which rules fired, and which NEVER have."
|
||||
],
|
||||
"hooks": {
|
||||
"InstructionsLoaded": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "L=\"$HOME/.claude/instructions-loaded.jsonl\"; if [ -f \"$L\" ] && [ \"$(stat -c%s \"$L\" 2>/dev/null || echo 0)\" -gt 5242880 ]; then mv -f \"$L\" \"$L.1\"; fi; jq -c '{ts:(now|todate), file_path, memory_type, load_reason, trigger_file_path, parent_file_path, globs, session_id, cwd}' >> \"$L\" 2>/dev/null || true",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user