Claude Code can load project instructions from CLAUDE.md, including an imported Learnings.md. That gives a team a place for lessons from earlier work, but a sentence in an old conversation is not evidence that a fix worked. This guide sets up an owner-run session close: export the finished conversation, check the relevant tool output and project artifacts, propose a few specific lessons, review the exact file change, and try one of those lessons on a real task in a different fresh session. Anthropic's sessions manual documents the readable /export route; its internal JSONL transcript format is subject to change.
The big change
- What changed: A completed coding task can feed a small project instruction file through a repeatable, reviewed process. The export provides an evidence trail; the owner decides which observations deserve to persist.
- Why it matters: A developer can retain a working correction without asking the next session to reconstruct an old conversation. The separate approval and bounded write keep plausible but unverified claims out of the project file.
- What to watch: The useful check is the next task's result against its actual file or test. Loading
Learnings.mdshows that the instruction was available; it does not show that Claude followed it or that performance improved.
Set up the project files
You need an owned Claude Code project, access to its project skills and instruction imports, permission to export and inspect your own session, and Python 3.11 or later. Check your installed Claude Code version, /skills, /permissions, repository policy, and where private exports may be stored. A later Claude session can count against your account's usage; this article performed no paid run.
From the project root, create .claude/skills/wrap-up-learnings/ and .claude/tools/. Copy the complete SKILL.md, recipe.md, and learnings.py printed below into these exact paths:
.claude/skills/wrap-up-learnings/SKILL.md
.claude/skills/wrap-up-learnings/recipe.md
.claude/tools/learnings.pyBack up or commit an existing CLAUDE.md and Learnings.md before editing. After checking the scope of the root CLAUDE.md, add @Learnings.md on its own line. Anthropic's memory manual says relative imports resolve from the file containing the import. Preserve an existing Learnings.md; the curator expects the heading and ID format shown below and holds on another format. To create a new UTF-8 file with the required LF newline, run this command from the project root. Its exclusive create mode refuses to overwrite an existing file:
python -c "from pathlib import Path; f=Path('Learnings.md').open('xb'); f.write(b'# Learnings\n'); f.close()"Keep exports, proposals and approvals in an owner-controlled private location. An imported file is project context, not secret storage. Decide separately whether the resulting Learnings.md belongs in Git; inspect its diff before sharing it.
Finish, export, and inspect one session
Finish the actual task and let its checks settle. In that same Claude Code conversation, use /export PRIVATE_PATH/session-SESSION_ID.txt, or run /export and save through the menu. Anthropic documents that the export contains readable messages and tool outputs. Use a fresh private path. The owner should read the relevant export lines and check any success or failure against the actual repository files, test results or other owned artifacts. If compaction or an omitted output leaves a necessary step unproved, hold that lesson or check the original artifact. Do not infer the missing result.
Then invoke /wrap-up-learnings manually with the project path, permitted export directory and exact export path. The skill calls a local script to index every raw line by number and SHA-256. It does not interpret a Claude transcript schema or decide which lines are true. Inspect the private lines yourself before preparing at most five durable additions and any explicit removals.
Review the proposal before writing
The private proposal.json names the source session, the SHA-256 of the whole export, the current Learnings.md hash, any IDs to remove and the lessons to add. Copy exportSha256 and each cited line number and hash from the generated .claude/lesson-evidence-SESSION_ID.json index. From the project root, get baseSha256 from the exact current file bytes:
python -c "import hashlib; from pathlib import Path; print(hashlib.sha256(Path('Learnings.md').read_bytes()).hexdigest())"Every lesson cites one to six exact export line hashes. A line that merely says “fixed” cannot establish a working fix; compare it with the underlying file or test. Explain any observed failure and limit. Show the owner the complete proposed diff and the cited evidence.
Only the owner should create a separate approval.json for the exact proposal bytes and exact ordered change IDs. After saving and reviewing proposal.json, get its file-byte hash from the project root for proposalSha256:
python -c "import hashlib; from pathlib import Path; print(hashlib.sha256(Path('proposal.json').read_bytes()).hexdigest())"Set approvedIds in the same order as proposal.json's add IDs and removeIds in the same order as its remove IDs. The local reviewed: true file is a workflow gate, not a cryptographic signature: someone able to change both local files can forge it. Protect the working directory and use ordinary independent Git review where appropriate. The script checks those hashes, the export, the base file, evidence references and bounds before writing. Run apply only after that review. If it reports HOLD or its response is uncertain, inspect Learnings.md and the lock before any retry.
The curator permits an 8 MiB export, a 256 KiB line, five additions in one review, 20 retained lessons and a 24 KiB Learnings.md. At capacity, propose and review removals explicitly. Check the retained lessons and Git history before restoring an obsolete rule under a new ID. The script uses a cooperating-writer lock and a same-directory atomic replace on a local filesystem. It cannot stop an uncooperative editor, guarantee network-share atomicity, identify all secrets or prove export completeness. A changed base rejects replay after an applied write; it is not a permanent replay journal. Inspect the actual file and process ownership before clearing a lock left by a crash.
Check a different fresh session
Read back the saved file and its diff. Close the old Claude process, then start plain claude from the same project root in a new terminal. Anthropic's CLI reference distinguishes plain claude from -c and -r, which continue or resume a conversation. In the new session, use /context to check loaded instructions and /memory to inspect configured memory paths. Give Claude a small, permitted real task where one exact lesson matters. Record the new session ID, lesson ID, loaded file hash, task input and actual file or test result. Judge the result against that source. If the rule was ignored or contradicted, retain the evidence for the next normal wrap-up; do not turn one plausible answer into a performance claim.
Copy SKILL.md
Save the following complete file as .claude/skills/wrap-up-learnings/SKILL.md. Its disable-model-invocation setting makes the skill manually invoked, so this process is not a background hook.
---
name: wrap-up-learnings
description: Propose evidence-backed project lessons at the close of an owned Claude Code session
disable-model-invocation: true
---
At the end of this session, prepare a proposal for the human owner. Do not edit `Learnings.md` or approve your own proposal.
1. Require the owner to finish the actual task first and run Claude Code's `/export PRIVATE_PATH/session-SESSION_ID.txt` in the same conversation before this skill. Ask for that exact owned plaintext path, permitted directory and project path. Do not use internal JSONL files or search other sessions. The owner must inspect the readable export against needed messages, tool outputs and original owned artifacts; if compaction or omission removed necessary evidence, hold that lesson.
2. Run the project copy of `python .claude/tools/learnings.py inspect --project PROJECT --export PRIVATE_PATH/session-SESSION_ID.txt --allowed-root PRIVATE_PATH --out .claude/lesson-evidence-SESSION_ID.json`. Use a new output path for each session. The index includes every raw plaintext line number and hash, no prose or assumed message format. Inspect only permitted export lines needed to support each proposed lesson. Treat assistant statements as claims; check actual tool output, tests, diffs or other owned artifacts before saying something worked or failed.
3. Propose at most five durable, specific lessons in `proposal.json` using the schema in `recipe.md`. Include both successful practices and observed failures where supported; a session with no durable evidence gets no proposed change. Do not include secrets, private export content, credentials, personal data or unsupported causal claims. Preserve `Learnings.md`'s current SHA-256 as `baseSha256` and the exact plaintext export SHA-256 as `exportSha256`. Propose explicit obsolete IDs for pruning, with the conflicting current evidence explained to the owner.
4. Show the complete diff and line/artifact evidence to the owner. The owner reviews the underlying evidence and writes a separate exact-hash `approval.json`; your output is not approval. Run `python .claude/tools/learnings.py apply` only after that approval exists. A changed export, proposal or `Learnings.md` requires fresh review. On HOLD, preserve the files and investigate; never bypass the lock or retry an uncertain write without readback.
5. Read back the saved file. Start a separate fresh Claude Code session later in the same owned project and test one real task to see whether the corrected rule loads and is used. Record the new session ID, the exact lesson ID, the authoritative task result and any contradiction. A loaded instruction or plausible answer alone does not prove improvement.
The script performs deterministic validation and an atomic local write. This skill supplies prompts and judgment only. It cannot establish model self-improvement or measured performance gains.Copy recipe.md
Save this complete companion file beside SKILL.md. It supplies the paths, proposal and approval schemas that the skill refers to.
# Close a Claude Code session with reviewed project learnings
**Reader and outcome.** A developer who owns a Claude Code project can repeatedly extract supported successes and failures from a saved plaintext export of each completed task, curate a small `Learnings.md`, and check in a different fresh session whether the corrected rule was actually used. The acceptance signal is a reviewed, hash-bound file change plus a later task result that the owner checks against its real source. No live Claude session, authenticated account, private export or model performance claim was tested for this guide.
**Basis and version.** Claude Code's current [sessions](https://code.claude.com/docs/en/sessions), [commands](https://code.claude.com/docs/en/commands), [skills](https://code.claude.com/docs/en/skills), [memory](https://code.claude.com/docs/en/memory) and [permissions](https://code.claude.com/docs/en/permissions) manuals were checked on 25 October 2026; these pages state no stable page version or publication date. The sessions manual documents `/export` as a plaintext rendering of the current conversation, with messages and tool outputs, and explicitly warns that internal JSONL rows change between releases. `SKILL.md` and `learnings.py` here are original local templates. Python 3.11+ and only the standard library are required. Before use, inspect the installed Claude Code version and the current docs, account access, permission mode, repository policy and export privacy. The code was tested only with synthetic plaintext files on Python 3.12. It does not call an API or model.
1. In an owned project, choose a private place for the export and proposal files. Back up or commit any existing `CLAUDE.md` and `Learnings.md` first. Copy this complete `SKILL.md` and `recipe.md` to `.claude/skills/wrap-up-learnings/`, and `learnings.py` to `.claude/tools/learnings.py`. Check `/skills` and `/permissions` in your actual Claude Code session. Installation is a reader action, not something BIG CHANGE performed. The frontmatter makes `/wrap-up-learnings` a manually invoked skill; Claude cannot invoke it on its own. The script has only project-file write authority granted by the invoking shell/user, so use normal OS and Claude permission controls to restrict it.
2. Put this one line in the project root `CLAUDE.md` after reviewing the file's existing content and scope: `@Learnings.md`. Anthropic documents `@` imports relative to the containing `CLAUDE.md`; a missing file or conflicting instruction must be resolved before relying on it. For a new file, create `Learnings.md` as UTF-8 without a BOM, containing exactly `# Learnings` followed by an LF newline. From the project root, `python -c "from pathlib import Path; f=Path('Learnings.md').open('xb'); f.write(b'# Learnings\n'); f.close()"` creates those bytes and refuses to overwrite an existing file. Preserve an existing file; this curator expects its own heading-and-ID format and will hold on an incompatible file rather than convert it silently. Do not paste sensitive session text into a shared repository. Decide whether `Learnings.md`, proposals and evidence indexes stay private or are reviewed for Git; `CLAUDE.md` imports are not secret storage.
3. After the real task and its tool outputs have settled, run `/export PRIVATE_PATH/session-SESSION_ID.txt` in that **same** Claude Code conversation, or use `/export` and save through its dialog. Choose a new private file path and verify the file exists. Do this **before** invoking `/wrap-up-learnings`; the wrap-up itself need not appear in the export. The owner reads the relevant rendered messages and tool outputs and compares them with the actual task, files and test artifacts. `/export` is a readable rendering, not an authenticated proof of truth or completeness. If compaction, truncation, missing tool output or another omission removes needed evidence, hold that lesson or obtain the original owned artifact; never infer the missing steps or search other private sessions. No Stop hook, background job, direct JSONL reader or paid subprocess is required.
4. Invoke `/wrap-up-learnings` with the owner-selected export path, allowed directory and exact project path. Run, with actual paths substituted: `python .claude/tools/learnings.py inspect --project PROJECT --export PRIVATE_PATH/session-SESSION_ID.txt --allowed-root PRIVATE_PATH --out .claude/lesson-evidence-SESSION_ID.json`. Use a unique output per session. The index records every raw plaintext line's number, byte length and SHA-256 plus the whole export hash, while copying no export text. The script assumes no role headers, tool-output delimiters or other rendered format. It rejects empty, binary/control, invalid UTF-8, oversized and `.jsonl` inputs. The index and export hash come from one bounded byte snapshot. The owner identifies the lines that matter by reading the actual export and checks any claimed success or failure against owned files, tests or authoritative sources. An export line saying “fixed” is not a verified fix.
5. Write a private `proposal.json` under the project root, at most five entries, using the exact schema below. `sessionId` is the exact source session identity recorded by the owner, `exportSha256` hashes the entire permitted plaintext export bytes, and `baseSha256` hashes the exact current `Learnings.md` bytes (SHA-256 of empty bytes if absent). Each evidence reference is a line number and full raw-line hash from the index. Describe observed failure and recovery honestly; a line that merely says “fixed” cannot establish a working fix. For pruning, list existing lesson IDs in `remove` only after checking the current authoritative conflict and show that evidence in the review diff. Never ask a model to approve itself.
~~~json
{
"schema": 1,
"sessionId": "actual-session-id",
"exportSha256": "64-hex-SHA256",
"baseSha256": "64-hex-SHA256",
"remove": [],
"add": [{
"id": "specific-stable-id",
"scope": "One exact project/tool condition",
"lesson": "Action to repeat, checked against a real result",
"failure": "Observed failed approach or explicit limit",
"evidence": [{"line": 42, "sha256": "64-hex-raw-export-line-SHA256"}]
}]
}
~~~
6. The human owner reads the original export lines, the complete proposed diff, actual artifacts and any conflicts. Their separate `approval.json` must name the exact proposal file hash, the same session ID and the exact ordered `approvedIds` and `removeIds`. It must contain `"reviewed": true`. This is a local file contract, not a Claude permission grant or a signature. Anyone who can forge or edit both local files can bypass it; protect the project and use independent Git review for a team repository. Do not place approval in the skill prompt. Run `python .claude/tools/learnings.py apply --project PROJECT --export PRIVATE_PATH/session-SESSION_ID.txt --allowed-root PRIVATE_PATH --proposal PROJECT/proposal.json --approval PROJECT/approval.json` once. A rejected or uncertain write calls for readback of `Learnings.md` and the lock state, not blind retry.
~~~json
{
"schema": 1,
"reviewed": true,
"sessionId": "actual-session-id",
"proposalSha256": "64-hex-SHA256-of-exact-proposal.json-bytes",
"approvedIds": ["specific-stable-id"],
"removeIds": []
}
~~~
7. The script locks cooperating curators with exclusive `.learnings.lock`, rechecks the base hash inside the lock, writes a same-directory temporary file, flushes it, atomically replaces `Learnings.md`, and reads it back. Its limits are 8 MiB export, 256 KiB per line, five additions per review, 20 retained lessons and 24 KiB `Learnings.md`. Existing IDs cannot be silently overwritten. An explicit reviewed prune is required at capacity. Before approving a new lesson, compare its task and failure mode with both retained lessons and the reviewed Git history so an obsolete instruction is not reintroduced under a fresh ID. Replaying a proposal that changed the file fails on the base hash, preserving the already applied result; this is a base-version guard, not a permanent session replay journal. A crash can leave the lock file; inspect the target and process ownership before manually clearing it. Atomic replace protects a single file on the local filesystem, not a network share or an uncooperative simultaneous editor; coordinate ordinary edits and review the Git diff. No Stop hook is installed, so a write cannot trigger another wrap-up turn.
8. After closing the old process, start plain `claude` from the same project root in a new terminal, without continue or resume. In this **different fresh session**, check `/context` for the applicable loaded instruction files and `/memory` for the configured paths and ask Claude to perform a small permitted task where the new lesson matters. Record the new session ID, actual loaded `Learnings.md` hash, exact task input, tool/result evidence and owner judgment in a private verification note. A successful task must be checked against the actual file/test or authoritative source; if the lesson was ignored or contradicted, record that failure and propose a correction at the next normal wrap-up. Do not claim automatic learning or a performance gain from file presence, one answer or synthetic tests. The next session may incur normal account usage charges; set account/provider spend controls and use a harmless low-cost task. BIG CHANGE performed no paid run.
**Privacy and failure boundaries.** Plaintext exports may hold prompts, tool output, secrets and personal data. Keep the export, indexes, proposals, approvals and verification records under the owner's access controls; the script never sends data over a network and never prints export text. A line hash proves byte identity, not truth or privacy clearance. The human must inspect and redact before sharing a lesson. The script's guard cannot recognize all secrets, cannot prove a rendered tool result was authentic or complete, cannot resolve conflicting instructions, and does not make Claude follow a lesson. `CLAUDE.md` import behavior and effective permissions must be checked in the reader's installation. If export access, necessary evidence or independent review is unavailable, the honest output is a held proposal with no write.Copy learnings.py
Save the following complete local script as .claude/tools/learnings.py. It uses only Python's standard library and does not contact a model or API.
#!/usr/bin/env python3
"""Local, human-gated Learnings.md curator. Python 3.11+; stdlib only."""
import argparse
import hashlib
import io
import json
import os
from pathlib import Path
import sys
import tempfile
MAX_EXPORT = 8 * 1024 * 1024
MAX_LINE = 256 * 1024
MAX_LEARNINGS = 20
MAX_FILE = 24 * 1024
def digest(data):
return hashlib.sha256(data).hexdigest()
def within(path, root):
p, r = Path(path).resolve(strict=True), Path(root).resolve(strict=True)
if p != r and r not in p.parents:
raise ValueError("path escapes allowed root")
return p
def project_target(project, relative):
if Path(relative).is_absolute() or ".." in Path(relative).parts:
raise ValueError("unsafe project-relative path")
root = Path(project).resolve(strict=True)
target = root / relative
if target.is_symlink() or any(p.is_symlink() for p in target.parents if p != root):
raise ValueError("symlink target or parent")
if target.resolve(strict=False) != root and root not in target.resolve(strict=False).parents:
raise ValueError("path escapes project")
return target
def load_json(path, limit=MAX_FILE):
data = Path(path).read_bytes()
if len(data) > limit:
raise ValueError("JSON too large")
return json.loads(data), data
def export_snapshot(path):
if path.suffix.lower() == ".jsonl":
raise ValueError("expected owner-created /export plain text, not internal JSONL")
with path.open("rb") as stream:
data = stream.read(MAX_EXPORT + 1)
if len(data) > MAX_EXPORT:
raise ValueError("export exceeds 8 MiB; choose a bounded task")
try:
decoded = data.decode("utf-8-sig")
except UnicodeDecodeError as exc:
raise ValueError("export is not UTF-8 plain text") from exc
if not decoded.strip() or any(ord(ch) < 32 and ch not in "\t\n\r\f" for ch in decoded):
raise ValueError("export is empty or contains binary/control bytes")
return data
def export_lines(data):
with io.BytesIO(data) as stream:
for number, raw in enumerate(stream, 1):
if len(raw) > MAX_LINE:
raise ValueError(f"export line {number} exceeds 256 KiB")
yield number, digest(raw), len(raw)
def safe_write(path, body):
path.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(prefix=".learnings-", dir=path.parent)
try:
with os.fdopen(fd, "wb") as stream:
stream.write(body)
stream.flush()
os.fsync(stream.fileno())
os.replace(name, path)
finally:
if os.path.exists(name):
os.unlink(name)
def inspect(args):
path = within(args.export, args.allowed_root)
snapshot = export_snapshot(path)
index = [{"line": n, "sha256": h, "byteLength": size}
for n, h, size in export_lines(snapshot)]
out = project_target(args.project, args.out)
if out.exists():
raise ValueError("refusing to overwrite an evidence index")
body = json.dumps({"format": "claude-plaintext-export-line-index-v1",
"provenance": "owner-identified-export; completeness unverified",
"exportSha256": digest(snapshot), "lines": index}, indent=2).encode() + b"\n"
safe_write(out, body)
print(f"indexed {len(index)} raw lines; export text was not copied")
def apply(args):
root = Path(args.project).resolve(strict=True)
export = within(args.export, args.allowed_root)
proposal_path = within(args.proposal, root)
approval_path = within(args.approval, root)
proposal, proposal_bytes = load_json(proposal_path)
approval, _ = load_json(approval_path)
if not isinstance(proposal, dict) or not isinstance(approval, dict):
raise ValueError("proposal and approval must be JSON objects")
target = project_target(root, "Learnings.md")
if proposal.get("schema") != 1 or approval.get("schema") != 1:
raise ValueError("unsupported proposal/approval schema")
if approval.get("proposalSha256") != digest(proposal_bytes) or approval.get("reviewed") is not True:
raise ValueError("proposal not exactly reviewed")
session_id = proposal.get("sessionId")
if (not isinstance(session_id, str) or not session_id.isascii() or
not session_id.replace("-", "").replace("_", "").isalnum() or
len(session_id) > 100 or session_id != approval.get("sessionId")):
raise ValueError("session mismatch")
raw_export = export_snapshot(export)
if proposal.get("exportSha256") != digest(raw_export):
raise ValueError("export changed")
existing = target.read_bytes() if target.exists() else b""
if len(existing) > MAX_FILE or proposal.get("baseSha256") != digest(existing):
raise ValueError("Learnings.md changed or exceeds 24 KiB")
entries = proposal.get("add")
remove = proposal.get("remove", [])
if not isinstance(entries, list) or not isinstance(remove, list) or len(entries) > 5:
raise ValueError("invalid bounded changes")
if any(not isinstance(e, dict) for e in entries) or any(not isinstance(key, str) for key in remove):
raise ValueError("invalid addition or prune ID type")
if not entries and not remove:
raise ValueError("empty proposal; no write needed")
if approval.get("approvedIds") != [e.get("id") for e in entries] or approval.get("removeIds") != remove:
raise ValueError("approved change set differs")
evidence = {n: h for n, h, _ in export_lines(raw_export)}
ids = set()
for entry in entries:
if not isinstance(entry, dict) or set(entry) != {"id", "scope", "lesson", "failure", "evidence"}:
raise ValueError("invalid lesson fields")
identifier = entry["id"]
if not isinstance(identifier, str) or not identifier.isascii() or not identifier.replace("-", "").isalnum() or len(identifier) > 64 or identifier in ids:
raise ValueError("invalid/duplicate lesson ID")
ids.add(identifier)
for field in ("scope", "lesson", "failure"):
value = entry[field]
if not isinstance(value, str) or not 1 <= len(value) <= 500 or "\n" in value or "\r" in value:
raise ValueError("invalid lesson text")
refs = entry["evidence"]
if not isinstance(refs, list) or not 1 <= len(refs) <= 6:
raise ValueError("evidence required")
if any(not isinstance(ref, dict) or type(ref.get("line")) is not int
or ref["line"] not in evidence or evidence[ref["line"]] != ref.get("sha256") for ref in refs):
raise ValueError("evidence line/hash mismatch")
text = existing.decode("utf-8")
if text and not text.startswith("# Learnings\n"):
raise ValueError("existing file has unknown format")
blocks = text.split("\n## ") if text else ["# Learnings\n"]
head = blocks[0].rstrip() + "\n"
previous = {}
for block in blocks[1:]:
key = block.split("\n", 1)[0]
if key in previous:
raise ValueError("duplicate existing ID")
previous[key] = block.rstrip() + "\n"
if len(remove) != len(set(remove)) or any(key not in previous for key in remove):
raise ValueError("invalid prune list")
for key in remove:
del previous[key]
if any(key in previous for key in ids):
raise ValueError("existing lesson ID; revise explicitly")
for e in entries:
refs = ", ".join(str(ref["line"]) + ":" + ref["sha256"][:12] for ref in e["evidence"])
previous[e["id"]] = (f"{e['id']}\nScope: {e['scope']}\nLesson: {e['lesson']}\n"
f"Failure/limit: {e['failure']}\nSource session: {proposal['sessionId']}\n"
f"Export line hashes: {refs}\n")
if len(previous) > MAX_LEARNINGS:
raise ValueError("over 20 lessons; explicit reviewed pruning required")
result = (head + "".join("\n## " + previous[k] for k in sorted(previous))).encode()
if len(result) > MAX_FILE:
raise ValueError("result exceeds 24 KiB")
lock = project_target(root, ".learnings.lock")
try:
fd = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError as exc:
raise ValueError("another curator owns lock; inspect before retry") from exc
try:
os.close(fd)
current = target.read_bytes() if target.exists() else b""
if digest(current) != digest(existing):
raise ValueError("concurrent Learnings.md change")
safe_write(target, result)
if target.read_bytes() != result:
raise ValueError("write readback mismatch")
print("applied reviewed lessons; sha256=" + digest(result))
finally:
lock.unlink()
def main():
parser = argparse.ArgumentParser()
sub = parser.add_subparsers(dest="command", required=True)
for name in ("inspect", "apply"):
command = sub.add_parser(name)
command.add_argument("--project", required=True)
command.add_argument("--export", required=True)
command.add_argument("--allowed-root", required=True)
if name == "inspect":
command.add_argument("--out", default=".claude/lesson-evidence-index.json")
else:
command.add_argument("--proposal", required=True)
command.add_argument("--approval", required=True)
args = parser.parse_args()
try:
(inspect if args.command == "inspect" else apply)(args)
except (OSError, ValueError, KeyError, UnicodeError) as exc:
print("HOLD: " + str(exc), file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
raise SystemExit(main())The reference implementation was checked with 11 synthetic Python 3.12 tests of the local indexing and write guards. BIG CHANGE did not run a real Claude /export, use a private account or export, start a fresh Claude session, or measure adherence or performance. Those remain the reader's checks in the installed environment.



