chore(memory): BDR-095 LRN-160 BLK-022, journal + TODO 2026-09-22
This commit is contained in:
@@ -248,3 +248,9 @@ rules:
|
||||
- **Real cause** (strong evidence, not reproduced on purpose): `/tmp` = tmpfs 7.4 GB mounted `usrquota`; `/tmp/claude-1000/-home-bchanot-Documents-claude/fefd277c-…/scratchpad` holds 5.9 GB of sandbox HOMEs (`pinprobe/` 2.1 GB, `pinrc/` 1.6 GB, `imp1 impg imptest sbx1 sbx2 v3.2.0 v3.6.1 v4.0.5 …`) from the impeccable pin probes. `dd` 40 MB to `/tmp/claude-1000` → "Disk quota exceeded" (EDQUOT) while `df` still shows 1.6 GB avail. Same write to `~/.cache` OK. Bash tool + `mktemp` + heredocs live in /tmp → all die together. This session: first impeccable probe failed with `Quota exceeded (os error 122)` on the installer's `/tmp/impeccable-update-*` staging, same cause.
|
||||
- **Solution**: this round ran everything with `TMPDIR=~/.cache/imp-probe/tmp` (probe, harness, `make test`). Durable fix = delete the dead session's scratchpad: `rm -rf /tmp/claude-1000/-home-bchanot-Documents-claude/fefd277c-e143-4d51-b589-a566641079b5` (agent's `rm -rf` on /tmp denied by the classifier → user action). Rule for probes: sandbox HOMEs that pull npm/node payloads go under `~/.cache/<probe>/`, never the /tmp scratchpad, and get removed at the end of the session.
|
||||
- **Status**: open until the user frees /tmp. Links [[BDR-094]], [[LRN-159]].
|
||||
|
||||
## BLK-022 — `hooks/guard-bash.sh` withheld by the safety classifier; executable spec shipped instead — 2026-09-22
|
||||
- **Friction**: layer C item G2 (PreToolUse Bash guard: whole-command scan incl. nested `bash -c`, `docker compose run … lftp`, scripts the command runs; exit 2 + reason + `logger` trace; fail-closed without jq). The response carrying the hook body was stopped by a safety classifier mid-write; content withheld, instruction not to regenerate it.
|
||||
- **Real cause**: the hook body is a dense list of destructive-command patterns (rm -r forms, disk tools, docker escapes, history rewrites); the classifier reads it as harmful capability regardless of the defensive frame.
|
||||
- **Solution**: `lib/tests/guard-bash.test.sh` (214 cases, deny/allow) stays as the spec and SKIPs while the hook is absent, so `make test` stays green. Options: user writes the hook against the spec (start from `/mnt/cloudpex/RECOVERY/01-prochain-systeme/claude-config/hooks/guard-bash.sh`, already on disk, then iterate to green); or a different design (allowlist of first words + path containment) requested explicitly. Until then: static deny (BDR-095) covers the direct forms; nested forms rely on the classifier prose.
|
||||
- **Status**: open. Links [[BDR-095]], [[LRN-160]].
|
||||
|
||||
@@ -1196,3 +1196,11 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
|
||||
- **Alternatives rejected**: before/after skill-version compare (first idea) → cannot separate rotted-pin no-op from up-to-date no-op, identical files + rc 0 both times → false warn on every rerun. `--force` → re-downloads ~15 MB engine + dist on every `make plugin`, and the CLI's own update check already refreshes without it. Shared `lib/impeccable.sh` for `imp_install` → deferred: two mirrored 12-line helpers vs new lib + test; revisit at a third caller. Project-scope install inside this repo → Claude Code scans `.claude/skills` too = skill listed twice, shadows the global copy (seen live, TODO T6).
|
||||
- **Status**: accepted. Verified: harness on extracted Step 8d, sandbox HOME, real installer, 4/4: fresh install (skill 4.3.1, 4 agents); rotted pin over a copy → fallback fires; same pin rerun → no false warn; parked + rotted → fallback, returned to `skills-disabled/`. `make test` green minus 2 pre-existing T16a (gitleaks absent), shellcheck clean. `update-all.sh` block: `bash -n` + shellcheck only, same helper, not run end to end.
|
||||
- **Reference**: `install-plugins.sh` Step 8d, `update-all.sh`, `plugins.lock.json`, `.gitignore`, `link.sh`, `lib/design-gate.md` §5. Links [[LRN-159]], [[LRN-158]] (21st: opposite case, installer refuses symlinks → stage), [[LRN-077]] (pin doctrine), [[BLK-021]].
|
||||
|
||||
## BDR-095 — Data-loss guardrails: static deny for transfer/destructive tools, push every commit, brief ≠ user authority
|
||||
- **Date**: 2026-09-22
|
||||
- **Decision**: layer C of the post-incident plan (layers A OS/backup and B sandbox/managed-settings = user's side). (1) `permissions.deny` static: lftp/sftp/ftp/curl -T, `rsync --delete`, `xargs rm`, pipe-to-shell, `chmod/chown -R`, sudo/doas/pkexec, dd/mkfs/shred/wipefs/fdisk/parted, chattr, docker volume drops/system prune/compose down -v/--privileged/docker.sock/`-v /:`, git push --delete/--mirror/:ref/--force-with-lease, branch -D, filter-branch, reflog expire, stash clear/drop, clean -f, --no-verify, core.hooksPath. `ask` entries for pipe-to-shell + stash drop/clear retired into deny. (2) `hard_deny`: destructive tool against a local path from a variable/`~`/`..`/wildcard/outside cwd+tmp, trace or rehearsal included, brief carries no user authority. (3) `soft_deny`: docker entry reworded (promoted items out), + discarding uncommitted work. (4) `environment`: incident, push discipline, Claude never deploys. (5) `lib/gitflow.sh`: `start` pushes `-u origin`, `_gitflow_merge_into` pushes target, `install-hook` writes post-commit + post-merge push hooks (`--follow-tags`, timeout 30, `GITFLOW_NO_PUSH=1`, warn-never-block). (6) `hooks/unpushed-guard.sh` SessionStart+Stop systemMessage. (7) doctrine section "Destructive tools & data loss" + 4 report-only agents clause. (8) `lib/tests/guard-bash.test.sh` = spec of the PreToolUse guard, hook not shipped ([[BLK-022]]).
|
||||
- **Why**: 21/09 wipe ([[LRN-160]]): prose tiers named neither lftp nor a local trace, the orchestrator's brief authorized it, auto mode inherited by the sub-agent, nothing pushed for 4 days. User: Claude never deploys, only explains; test = dev server on this machine → lftp has zero legitimate use. Static deny resolves before the classifier and inside sub-agents (doc verified 2026-09-22); prose is judgment, static is a rule.
|
||||
- **Alternatives rejected**: `ask` tier → doc says it prompts under auto, LRN-155 probe says inert, unresolved → nothing entrusted to ask. Keep docker volume drops in soft_deny (BDR-092) → "à tout prix" beats in-turn convenience; user runs them by hand. Post-commit hook alone for push → `git merge` fires post-merge, not post-commit (T18f caught it) → lib pushes the target explicitly AND post-merge hook emitted. Force-push allowance after amend → static deny stays; a rejected push warns and the user decides. Stop-hook `decision: block` on unpushed work → BDR-083 refused control-flow hooks; systemMessage only.
|
||||
- **Status**: accepted, on feature/destructive-guardrails. `gitflow-test.sh` T18 7/7 + T19 3/3, unpushed-guard 9/9, `make test` green minus 2 pre-existing T16a, shellcheck clean, doctor 0 errors. NOT DONE: `hooks/guard-bash.sh` ([[BLK-022]]). Existing projects need `gitflow install-hook` re-run for the push hooks.
|
||||
- **Reference**: `settings.json`, `lib/gitflow.sh`, `.githooks/{post-commit,post-merge}`, `hooks/unpushed-guard.sh`, `lib/tests/{guard-bash,unpushed-guard}.test.sh`, `CLAUDE.global.md`, `templates/settings/SETTINGS.md`, `agents/{verifier,plan-challenger,analyzer,security-auditor}.md`. Extends [[BDR-090]] [[BDR-092]] (ask inert, soft_deny doctrine); links [[LRN-114]] (T19 drift gate), [[LRN-155]], [[BDR-083]].
|
||||
|
||||
@@ -486,3 +486,6 @@ rules:
|
||||
- impeccable install repaired ([[BDR-094]]): global scope through the symlinks, 4 agents kept, pin 3.2.0 → 4.1.0 with @latest fallback, design-gate §5 `/impeccable init` hint. Residue probed: rotted pin over an existing copy exits 0 → `imp_install` reads the installer output ([[LRN-159]]); before/after version compare rejected (identical no-op). Harness 4/4, sandbox HOME, real installer.
|
||||
- Previous shell death traced: /tmp tmpfs usrquota blown by 5.9 GB of dead-session probe HOMEs ([[BLK-021]], open, user frees). Tests + harness ran with TMPDIR under ~/.cache. `make test` green minus 2 pre-existing T16a, shellcheck clean. Committed on feature/21st-cli-migration, UNMERGED. `skills/synced/` (claude.ai synced skills, 4.4 MB) untracked + unignored, left for the user.
|
||||
- Both lots (21st CLI migration + impeccable repair) merged into develop on user go, `gitflow finish` → 33e0899, pushed to origin. Feature branch deleted by the lib. Machine-owned `skills/impeccable`, `skills/graphify`, `agents/impeccable-*.md` verified still on disk after the merge (LRN-154 class).
|
||||
- Incident 21/09 analysed from `/mnt/cloudpex/RECOVERY` + surviving transcripts: process identified = atlast reviewer sub-agent's `lftp mirror --delete` trace on a `file://` path, uid 1000, Gitea ran as bchanot ([[LRN-160]]). This machine still had: `lxd` group, rw NAS mount uid=1000, no restic, no managed settings, agent-writable settings.json. Layers A/B handed to the user.
|
||||
- Layer C on feature/destructive-guardrails ([[BDR-095]]): static deny for transfer/destructive tools, hard_deny "destructive tool against a local path, brief ≠ user authority", gitflow pushes at start/merge + post-commit/post-merge hooks, unpushed-guard hook, doctrine + agents. T18 caught that `git merge` skips post-commit. Guard hook body withheld by the safety classifier → spec-only ([[BLK-022]]). Branch UNMERGED.
|
||||
|
||||
|
||||
@@ -1506,3 +1506,10 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
|
||||
- **Pattern**: two classes of npm pin. (a) self-contained package → pin freezes behaviour, rc is truth. (b) package that fetches its payload at install time (impeccable, ctx7, `skills add` style) → pin freezes only the fetcher; payload can vanish or drift. For (b): pin + `@latest` fallback + loud "bump the lock" warn, never pin-or-die. And when the tool has an "already installed" branch, capture stdout+stderr and match the failure text; rc and before/after compare both read "unchanged" for a no-op AND for a swallowed failure.
|
||||
- **Future application**: any `install-plugins.sh` step whose pinned tool downloads something at install time. Probe both HOME states (clean, copy present) before trusting rc. Cheap recipe: sandbox HOME with the repo-shaped symlinks, pinned install twice, then the rotted pin; diff rc + output + `stat`/`sha256sum` of the landed file.
|
||||
- **Reference**: [[BDR-094]], `install-plugins.sh` Step 8d `imp_install`, `update-all.sh`. Links [[LRN-077]] (why pin), [[LRN-034]] (run the real thing), [[LRN-158]].
|
||||
|
||||
## LRN-160 — Prose guardrails are judgment, not boundary: a well-argued brief walks a sub-agent through them
|
||||
- **Date**: 2026-09-22
|
||||
- **Context**: 2026-09-21 00:21, old server. Reviewer sub-agent (opus, atlast SDD task 26) briefed by the orchestrator: "Tracing lftp semantics against a scratch tree of your own making, outside the repository, is allowed". It ran `mirror --reverse --delete` against a local `file://` tree; target resolved to a real path; `mirror --delete` = `rm -r` on target dirs absent from source, `--exclude` ignored. 90 s: home, `~/.claude`, `/tmp` outputs, NAS (`uid=1000`), 15 Gitea repos (Gitea ran as bchanot = uid 1000, no Docker bridge needed). Reviewer's next Bash rc 1 with its output file gone, then API "Not logged in" (credentials wiped). Config of the day already had hard_deny "deploy to provider" + soft_deny `rsync --delete`: neither names lftp nor a local trace. 4 days of faunosteo never pushed; Gitea on the same disk.
|
||||
- **Pattern**: (a) an LLM classifier reads intent; the orchestrator's brief IS the sub-agent's user voice, so a reasoned authorization passes. Only static deny rules (resolve first, inherited by sub-agents, per-segment on `&&`) and OS rights are boundaries. (b) "Trace what it would do" is execution; a scratch target from a variable is one unset var away from `/`. (c) The event deletes its own evidence when the agent's uid owns the logs, the config and the transcripts. (d) A remote backs up only what it holds: push at branch creation and at every commit, from a hook, not from discipline. (e) `git merge` fires post-merge, not post-commit.
|
||||
- **Future application**: any new destructive capability → static deny first, prose second, doctrine third. Any orchestrator brief → never "X is allowed outside the repo". Sub-agent tools: report-only agents trace by reading. Probe a guard with the real sub-agent path (auto mode inherited), not the main session.
|
||||
- **Reference**: [[BDR-095]], `/mnt/cloudpex/RECOVERY/00-incident/`, atlast transcript `26e76a0b…` + stub `agent-a7d9119…`. Links [[BDR-090]], [[BDR-092]], [[LRN-155]], [[LRN-114]].
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
# TODO
|
||||
|
||||
## 2026-09-22 — destructive guardrails after the 21/09 wipe (feature/destructive-guardrails)
|
||||
Incident 2026-09-21 00:21 on the old server: a reviewer sub-agent (atlast SDD, opus)
|
||||
traced `lftp mirror --reverse --delete` against a local `file://` tree; the target
|
||||
resolved to a real path, `mirror --delete` did `rm -r` (ignores `--exclude`) on
|
||||
everything uid 1000 owned: home, `~/.claude`, NAS (uid=1000), 15 Gitea repos
|
||||
(Gitea ran as bchanot). The 17/09 classifier prose (hard_deny "deploy", soft_deny
|
||||
`rsync --delete`) named neither lftp nor a local trace; the orchestrator's brief
|
||||
authorized the trace; auto mode is inherited by sub-agents. 4 days of faunosteo
|
||||
never pushed. User decisions: Claude NEVER deploys (explains only), lftp has no
|
||||
use in session; layer A (OS, restic, NAS) and layer B (sandbox + managed
|
||||
settings) are the user's; this branch = layer C (config repo) + auto-push.
|
||||
- [x] G1 `lib/tests/guard-bash.test.sh` (214 cases, SKIPs while the hook is absent): transfer tools, mirror/sync
|
||||
delete, recursive rm outside cwd/tmp or via variable, chmod/chown -R,
|
||||
sudo, disk tools, docker privileged/system mounts/volume drops, git
|
||||
history destruction, forbidden write zones, guardrail tampering,
|
||||
nested forms (`bash -c`, `&&`, `docker compose run … lftp`), script
|
||||
files run by the command; allow list of ordinary commands.
|
||||
- [ ] G2 BLOCKED (BLK-022, safety classifier withheld the body) `hooks/guard-bash.sh`: PreToolUse Bash, exit 2 + reason,
|
||||
`logger` trace, fail-closed without jq.
|
||||
- [x] G3 `settings.json` (hook registration = unpushed-guard only, guard-bash pending): static `permissions.deny` (lftp/ftp/sftp, rsync
|
||||
--delete, chmod/chown -R, sudo/doas/pkexec, dd/mkfs/shred/…, docker
|
||||
prune/volume rm/down -v/--privileged/docker.sock, git push
|
||||
--delete/--mirror/:ref, branch -D, filter-branch, reflog expire, stash
|
||||
clear/drop, xargs rm, pipe-to-shell), hook registration, new
|
||||
`hard_deny` (destructive tool against a local path, brief ≠ user
|
||||
authority), `soft_deny` reworded (docker items promoted, discard of
|
||||
uncommitted work), `environment` lines updated.
|
||||
- [x] G4 `lib/gitflow.sh` (+ post-merge: `git merge` skips post-commit, T18f) : `start` pushes the branch (`-u origin`),
|
||||
post-commit hook emitted + installed with pre-commit (push every commit,
|
||||
`--follow-tags`, timeout, `GITFLOW_NO_PUSH=1` opt-out, never fails the
|
||||
commit), `install-hook`/`emit-hook` cover both; `.githooks/post-commit`
|
||||
in this repo; `gitflow-test.sh` T18.
|
||||
- [x] G5 `hooks/unpushed-guard.sh` on SessionStart + Stop: warns when the
|
||||
branch is ahead of origin or has no upstream; test.
|
||||
- [x] G6 doctrine: `CLAUDE.global.md` Security "Destructive tools & data
|
||||
loss" + gitflow auto-push line; the 4 read-only agents get the
|
||||
"trace by reading, never by running" clause.
|
||||
- [x] G7 docs: `templates/settings/SETTINGS.md` (hook tier, ask caveat),
|
||||
CHANGELOG, BDR-095, LRN-160, journal. `make test` + shellcheck.
|
||||
Existing projects: re-run `gitflow install-hook` to get post-commit.
|
||||
Out of scope here (user's side): restic append-only, lxd group, NAS mount,
|
||||
managed-settings.json + sandbox, per-project accounts, docker rootless.
|
||||
|
||||
## 2026-09-22 — impeccable install repaired: global scope + agents + rotted pin (feature/21st-cli-migration)
|
||||
User: `make plugin` never installs impeccable, it just prints "run it
|
||||
yourself"; running it by hand needs `--scope=global` to land right, and then
|
||||
|
||||
Reference in New Issue
Block a user