Repository navigation
fix(guard): protect ai-memory, Muse Code and Codex dotenv stores (A2 G8-A02) - #1240
Open
seathatflowsinourveins wants to merge 12 commits into
Open
seathatflowsinourveins wants to merge 12 commits into
seathatflowsinourveins wants to merge 12 commits into
Conversation
…G8-A02) A2 audit finding G8-A02 (P1): the secret-path guard, the user settings template, the credential inventory and the Muse file fence named no ai-memory or Muse Code store, and Codex's dotenv file was covered only by the generic .env reader rule. - scripts/hooks/secret_path_guard.py: three STORE_PATHS patterns appended after index 16: ai-memory/auth-token, auth-header and auth-tokens/ (service_secret_path), Muse's .config/muse/auth.json and Codex's .codex/.env (native_store_path). The ai-memory hook commands, which name only --data-dir, and a sibling .env.example still pass. adoption/hooks/claude/SHA256SUMS re-pinned. - adoption/templates/claude.settings.template.json: Read(~/...), Read(**/...) and Edit(~/...) denies for each store, ahead of the .env carve-outs. - adoption/credential-inventory.json: five protection-only entries. - tools/muse_file_fence.py: .local/share is a protected home input. - docs/decisions/2026-10-02-new-wsl-client-configuration.md: addendum with the new piece counts (419 pieces, 226 practice). Sources: akitaonrails/ai-memory v3.1.0 (11ca7c5b) crates/ai-memory-cli/src/config.rs:3347, :3359 and src/server_profiles.rs:30; openai/codex rust-v0.163.0 (83086188) codex-rs/arg0/src/lib.rs:270-276. Tests fail on 90471d6 with only the two test files overlaid and pass here: tests.test_secret_path_guard (test_g8_a02_*, linear-time leads) and tests.test_muse_file_fence (test_local_share_home_input_refused). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fold 2 of PR #1240 (A2 finding G8-A02) on 8d735d9: the nine findings of the Claude security read (CHANGES_REQUESTED) and the Muse read (P3). - P2-1, P2-2, P2-3: the Muse auth.json, Codex .env and Codex auth.json patterns (STORE_PATHS 18, 19 and 3) also read the ${XDG_CONFIG_HOME:-...} and ${CODEX_HOME:-...} spellings, as LinearScans through default_run_scan like index 1; the regular-expression equivalence test covers all four new scans. - P3-1: ai-memory's pattern (17) also reads $AI_MEMORY_DATA_DIR and ${AI_MEMORY_DATA_DIR:-...} (ai-memory v3.1.0 config.rs:760); POINTER_VARIABLE gains AI_MEMORY_DATA_DIR, the pointer variable of the four ai-memory inventory rows. - P3-2: <data_dir>/auth.json (config.rs:3028-3038, :3064-3075): pattern, Read/Edit denies and the ai-memory-provider-auth inventory row. - P3-3: a reader of the ai-memory data directory or the Muse config directory itself, or a glob over its top level, is refused (the HF_HOME_ROOT precedent); `ai-memory backup --to FILE` still passes. - P3-4: tests for the default and override spellings, the directory reads and the six inventory rows; the carve-out comment is corrected. - P3-5: the auth-tokens row points at upstream's first profile, default (config.rs:996, :1220). - P3-6: .claude/settings.json and the docs/secret-storage.md hand-merge block carry the 18 rules. SHA256SUMS re-pinned; the record addendum states 422 pieces (229 practice); manifest rows re-registered. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fold 3 of PR #1240 (A2 finding G8-A02) on e4309ef, the two blocking findings of the CC verdict CC-DELTA-e4309efc.md (findings 1 and 2 of MICRO-delta-e4309efc.md). - Finding 1 (P2): ai-memory's data-directory override is a directory, and the pointer-variable field means the path of a store file. The four ai-memory rows now read it the way the CODEX_HOME and HF_HOME rows read theirs, as a path-template default: ${AI_MEMORY_DATA_DIR:-$HOME/.local/share/ai-memory}/<file>, with pointer_variables []. scripts/credential_status.py accepts that one extra template prefix; expand_template, which the checker, credential_run.py and canary_proof.py all use, already resolves it. With the variable exported, canary_proof.py's pointer_paths no longer lists the directory (so prepare binds no directory pointer), its exclusions hold the four token files under it as key paths instead of the directory, and credential_run.py no longer strips the variable from child environments. The guard keeps refusing reads through the variable (POINTER_VARIABLE keeps the name; its comment says why). - Finding 2 (P3): the STORE_ROOTS comment now states the rule exactly. Tests: tests.test_canary_proof UnitTests.test_ai_memory_data_dir_override_is_no_pointer and StabilityTests.test_st9_ai_memory_data_dir_override_passes_prepare (needs a reviewed ripgrep), and the updated tests.test_secret_path_guard test_g8_a02_inventory_rows. SHA256SUMS re-pinned; manifest rows re-registered. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…rride Fold 4 of PR #1240 (A2 finding G8-A02) on 0378825, the P2 of the CC verdict CC-DELTA-03788258.md (MICRO-delta-03788258.md). Fold 3 made the four ai-memory rows' templates follow the data-dir variable while the canary U8 root stays at the default directory, so an exported non-default value moved the exclusions away from the root that is scanned and dropped the checker's override warning. The shape chosen is the CC's first one, the smallest that keeps every reader consistent: - The four rows keep the fixed default path. This stack pins --data-dir to it wherever it runs ai-memory, and the flag wins over the variable (akitaonrails/ai-memory v3.1.0 crates/ai-memory-cli/src/config.rs :2349-2355; the variable is read at :760). - AI_MEMORY_DATA_DIR moves to a new optional, warning-only field, override_variables. scripts/credential_status.py validates it and native_store_overrides reads it, so the checker reports store_path_overridden again. canary_proof.py and credential_run.py do not read it: no scan root, exclusion, pointer binding or child environment follows it. - The TEMPLATE_PREFIXES addition of fold 3 is reverted. - The guard keeps refusing reads through the variable; the pointer variable test now also covers override_variables. The other shape, U8 following the same template with exclusions under every root it walks, was not taken: it moves a scan root after a value the stack never passes to ai-memory. Two claims of fold 3 corrected: the rows' shape is not the CODEX_HOME and HF_HOME shape (Codex roots and exclusions both follow the registered homes; U8 has no equivalent), and the variable does not move the data directory for this stack's own invocations, whose flag wins. Test: tests.test_canary_proof UnitTests.test_ai_memory_data_dir_override_is_no_pointer exports the variable to a non-default directory and asserts the token files under every U8 ai-memory root are keys and the checker reports the override; it fails on 0378825. SHA256SUMS re-pinned; manifest rows re-registered. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ride Fold 5 of PR #1240 (A2 finding G8-A02) on 0c21a1d, B1-B4 of the CC verdict CC-DELTA-0c21a1d1.md (security read MICRO-security-0c21a1d1.md). - B1 (P2): the four ai-memory token names are key exclusions under the default and also under the override variable's absolute, non-blank value (scripts/credential_status.py override_paths, used by tools/credentials/canary_proof.py exclusions, the one list prepare digests and the scan uses). No scan root moves. The three claims that this stack pins --data-dir everywhere are narrowed to the settings template's hooks and the drafted launchd template; runs without the flag follow the variable, and the exclusions now cover them. - B2 (P2): the data-dir resolution is cited at the pinned tags, from the vendor's source at each tag: v2.4.1 (Linux pin, 433a19f3) crates/ai-memory-cli/src/config.rs:1358-1364 with the variable at :573, and v2.3.2 (macOS pin, 353841d9) :1137-1143 and :462. v3.1.0 (11ca7c5b, installed on the WSL host) agrees at :2349-2355 and :760. The token-file locators are given at the pinned tags too; auth-tokens/ comes from v3.1.0 only. - B3 (P3): the override warning fires only for a non-blank value other than the default, as ai-memory's env_string reads it (v2.4.1 config.rs:2086-2095, v2.3.2 :1808-1817). - B4 (P3): the store_path_overridden comment is corrected, the checker paragraph of docs/secret-storage.md names override variables, and the key-management decision gains a dated addendum that defines override_variables beside pointer_variables. Tests, failing on 0c21a1d: tests.test_canary_proof UnitTests test_ai_memory_tokens_written_under_a_moved_data_dir_are_keys_under_every_root and test_ai_memory_override_warning_needs_a_non_blank_value_other_than_the_default. SHA256SUMS re-pinned; manifest rows re-registered. Co-Authored-By: Claude Opus 5.5 <[email protected]>
8 tasks done
…fused Fold 6 of PR #1240 (A2 finding G8-A02) on 7705ba7, B1-B5 of the CC verdict CC-DELTA-7705ba78.md (reads MICRO-security-7705ba78.md and MICRO-muse-7705ba78.md). - B1 (P2): ai-memory canonicalises its data directory (v2.4.1 crates/ai-memory-cli/src/config.rs:1366, v2.3.2 :1145, v3.1.0 :2357) and the scan worker excludes by exact bytes against children built from each root's string. override_paths now returns each moved file as written, normalised, resolved through symlinks, and re-anchored on each scan root's own path; canary_proof.py exclusions passes the roots. No scan root moves. - B2 (P2): measured first-hand with ai-memory 3.1.0 init, keyless, in a throwaway HOME and working directory: with AI_MEMORY_DATA_DIR empty or blank the token files land in the working directory. figment 0.10.19 (the crates.io crate matching ai-memory's Cargo.lock at v2.3.2, v2.4.1 and v3.1.0) keeps empty values, src/providers/env.rs:504-519, merged at v2.4.1 config.rs:1318; canonicalise_or_keep keeps "" (:2254-2266). The checker now warns for any set value other than the default, and prepare refuses an empty, blank or relative value with override_variable_not_absolute. The four claims are corrected. - B3 (P3): the default is described as ai-memory's dirs::data_local_dir()/ai-memory (v2.4.1 :2248-2252; dirs 5.0.1 src/lin.rs:11-12, src/mac.rs:7, :13) and a config.toml data_dir (:1316); the inventory path is this stack's --data-dir value, and those moves are not modelled. - B4 (P3): the digest claim is corrected (prepare's digest and each scan's plan are both built by exclusions(); no step compares them), and the scan-time test docstring is narrowed. - B5 (P3): the guard comment no longer calls the variable warning-only. Tests failing on 7705ba7: tests.test_canary_proof UnitTests test_ai_memory_moved_tokens_are_excluded_in_their_canonical_spellings, test_ai_memory_override_refusal_for_empty_blank_or_relative_values and test_ai_memory_override_warning_for_any_set_value_other_than_the_default, plus the prepare refusal in test_st9_ai_memory_data_dir_override_passes_prepare (needs a reviewed ripgrep). SHA256SUMS re-pinned; manifest rows re-registered. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…th sides Fold 7 of PR #1240 (A2 finding G8-A02) on c467543, B1-B3 of the CC verdict CC-DELTA-c4675431.md (security read MICRO-security-c4675431.md), by the verdict's RULING: match on canonical forms on both sides instead of enumerating spellings. - B1 (P2): the scan worker (tools/credentials/canary_scan_worker.py) takes each walked root's canonical path from the directory descriptor it opened (fd_path, /proc/self/fd/<fd>; the proof runs on Linux only) and excludes an entry when either its literal spelling or canonical_root + "/" + rel matches, for exact rules and for a glob rule's parent. Prepare (canary_proof.py exclusions) adds canonical_exclusion(path) = realpath(dirname) + "/" + basename beside every literal exact exclusion and glob directory. The four-spelling enumeration in scripts/credential_status.py override_paths is dropped; it returns the literal path only. A trailing-slash CODEX_HOME, a trailing-slash HOME with the variable unset (the routed case, closed by the same rule) and a trailing-slash glob directory now match. - B2 (P3): the final component is never resolved, so a symlink planted at a token name keys only itself; its target directory is walked. - B3: the fold-6 commit's B1 bullet described the four-spelling enumeration, which this fold replaces; key-management.md and the inventory note now state the canonical-on-both-sides rule, naming dir and glob roots as tested and the tasks root as covered by the same code but not separately tested. Tests, through the worker's own walk (canary_scan_worker.Walk.traverse, no scanner): test_ai_memory_moved_tokens_are_keyed_by_the_worker_in_every_spelling (seven shapes plus a glob-root case; the two trailing-slash shapes and the glob case fail on c467543) and test_ai_memory_symlink_at_a_token_name_keys_only_itself (fails on c467543). The end-to-end StabilityTests test_st9_ai_memory_moved_tokens_are_never_opened_end_to_end needs a reviewed ripgrep and setpriv and is skipped on this host and in CI. Manifest rows re-registered; the guard is unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…nical form, fail-closed canonical path Fold 8 of PR #1240 (A2 finding G8-A02) on 98e94f0, the CC verdict CC-DELTA-98e94f0b.md (Claude read MICRO-delta-98e94f0b.md, Muse read MICRO-muse-98e94f0b.md). Scanner-class PR: findings cited by path and id. - B1 (P2; MICRO F4, Muse P2-1): the new ST9 end-to-end test (tests/test_canary_proof.py, StabilityTests) wrote the canary before prepare, so under a reviewed ripgrep it raised TypeError and pinned nothing. It now writes placeholders before prepare and the canary after it, and the unreachable doubled-slash forbidden entries are dropped (the seam builds dir_fd paths from readlink of the descriptor). Both ST9 tests now run and pass under ripgrep 15.2.0 (official release, sha256 checked). - F1 (P3): a glob rule's directory is canonical by its whole realpath (canary_proof.py canonical_exclusion, directory=True); exact rules keep realpath(dirname) + basename. New test for a symlinked glob directory. - F2 and Muse P3-1 (P3): an opened root whose descriptor has no path now ends the walk as walk_error (canary_scan_worker.py Walk.traverse, directory and file roots), never literal-only matching. Seam test forcing fd_path to None, plus fd_path direct cases. - F3 (P3): kept by the ruling; the residual is stated in docs/decisions/2026-09-29-key-management.md and adoption/credential-inventory.json. - F5 and Muse P3-3 (P3): g8_walk builds the plan with the sink's group and the current pointer paths; the glob-parent case moves to an agent-group A4 walk. - F6 (P3): new tests for a file root, a glob root, a glob parent and an exact rule that only the worker's canonical path meets, and for canonical_exclusion's forms; eleven G8-* mutants in tests/canary_mutants.py, each killed by a named test. - F7 (P3): key-management.md and the inventory note say scan_sink builds the forms at each scan (prepare records a digest only), and list file roots as tested and git and tasks roots as served and untested. - F8 (P3): the G8 unit tests that need /proc carry skipUnless(LINUX). - Muse P3-2 (P3): recorded in key-management.md: the macOS F_GETPATH branch is not implemented, as the proof refuses non-Linux platforms. - Q2: docs/decisions/2026-10-15-credential-term-grace.md locators re-pointed to canary_proof.py:1454-1455 and credential_status.py:361. - manifests/evidence.json: six rows refreshed. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
seathatflowsinourveins
marked this pull request as ready for review
October 17, 2026 18:24
# Conflicts: # adoption/hooks/claude/SHA256SUMS # manifests/evidence.json
# Conflicts: # manifests/evidence.json
Fold on ac01e51 for the native codex review's P2: canary_scan_worker.py's fd_path() and the decision addendum now cite microsoft/[email protected] (d504d40a) fs/d_path.c and man-pages-6.19 for the appended suffix, and state how the ambiguous suffix is handled. 1 of 1 findings covered. Built by GPT (cx/gpt-6.1-sol, max) through the Codex worker profile; committed, gated and pushed by the CC. The CC removed the build's in-tree PR-body copy (.github/pr-body-1240.md), its manifest row and the decision paragraph that named it: the body is on the PR. The sandbox had no writable scratch for it then. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Finding statuses from the builder's report: - R-1240-1: fixed - R-1240-2: fixed - NC-1240-1: recorded - R-1240-3: recorded - F3: recorded Built by GPT through the Codex worker profile (stack-worker, effort max); committed, fail-first tested, gated and pushed by the CC's script, whose results are in the PR body. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scope
Closes A2 finding G8-A02 by protecting the registered ai-memory, Muse Code and Codex stores in the guard and maintained deny copies. The inventory, metadata checker and canary exclusions carry the same additions; canary exclusions match both literal and canonical spellings.
90471d63f20918efa3a63ef949caffd8c012656c.f2462c3717fb30786d93ba433dda610129426a20, the reviewed Fold 9 head. Fold 9's parent wasac01e51b4293b1bd6aa15e01f2611d5de5610a39, the merge oforigin/mainata887cc4bbc5825391f622bb1fd71580ea97511fbinto the reviewed42e8b821head.lane:foundation.The implementation and registered store definitions are cited by repository path and line only. No credential path or value is reproduced in this body.
Final behavior
override_variablesis distinct from a file pointer: the checker warns for any set data-directory override other than the stack default, including empty and blank values; prepare refuses an empty, blank or relative override.scan_sinkbuilds canonical exclusion forms against the live filesystem at each scan. An exact exclusion resolves its parent and preserves its final component; a glob exclusion resolves its whole directory. A symlink at an excluded name keys that link, while its ordinary target inside a covered root is scanned as itself.walk_error, making the sink incomplete. A literal suffix on a file root's own basename is preserved separately.Fold 9: sealed finding NC-1240-1
NC-1240-1 (P2), fixed. The code comment and decision addendum now cite the Linux kernel matching this host and the pinned man-pages release for the descriptor-path mechanism. The kernel explicitly calls the deletion suffix ambiguous.
fd_pathalready refuses every suffixed target, including a live file or directory whose actual name ends in(deleted); it never strips the suffix or tries to disambiguate it with a second lookup. Its production callers use directory descriptors and fail closed onNone. A file root's basename is joined to the accepted parent separately and kept verbatim.proc_pid_fd(5)at the supplied release documents descriptor links andreadlink, but does not document the deletion suffix. The addendum identifies its exact documentation inproc_pid_exe(5)andproc_pid_maps(5), bounds those pages to their interfaces and cites the kernel call chain for descriptor-link behavior.Fold 9 changed no executable behavior or tests. Its executable AST matches
ac01e51bafter removing docstrings. The brief required a new failing regression only if behavior changed, so no fail-first overlay was needed. Fold 9 closed its one sealed finding.Fold 10: sealed verdict at f2462c3
f2462c37head. The 12,308 result belonged to the intermediate builder tree that included an extra PR-body file and its evidence row.link_outclaim overlooks the worker's return for an excluded entry before following its link. This fold leaves the deferred item open.The decision's SHA256 and byte count were refreshed once through
scripts.host_receipts.register_filefor both corrections. The worker bytes and all tests matchf2462c37; no behavioral fix or new regression test is involved. Both blocking findings are addressed.SOTA sources
f2462c3717fb30786d93ba433dda610129426a20,manifests/evidence.json:5012,18565,65870. Counting the final-head files array yields 12,307 entries; comparison withac01e51b4293b1bd6aa15e01f2611d5de5610a39yields exactly two changed rows, for the decision and worker, and no PR-body row. The sealed resolution isMICRO-gpt61sol-resolution-f2462c37.md, SHA256ed70562ea258a7c43a7b8b5a18c2beb7686adf5fc0a4d75304f7acdc1f3beb4a.d504d40ab83839cb61942a812a99d183c42227b9:fs/proc/fd.c:174-189,211-214,fs/proc/base.c:1823,1838-1854, andfs/d_path.c:255-256,287-288,318-325. The host'suname -rreports6.18.54.1-microsoft-standard-WSL2. These sources establish descriptor-path rendering and its ambiguous suffix.adb436b2e4471218021d86f188fb58dec3946eb8:man/man5/proc_pid_fd.5:13-23for descriptor symlinks andreadlink;man/man5/proc_pid_exe.5:23-25for the suffix on executable links;man/man5/proc_pid_maps.5:151-153for the suffix and ambiguity on mappings. The latter two pages do not establish descriptor-link semantics on their own.90471d63f20918efa3a63ef949caffd8c012656c:docs/decisions/2026-09-24-secret-storage.md,docs/decisions/2026-09-27-claude-harness-settings.mdanddocs/secret-storage.md.v2.4.1(433a19f3d54dea287571b1423591db2a89965fa9),crates/ai-memory-cli/src/config.rs:573,1358-1364,2086-2095,2248-2266;v2.3.2(353841d91618d20b110b208de284a74d0b960379), the same file at:462,1137-1145,1808-1817,1970-1974; andv3.1.0(11ca7c5b3b65e7fc216c7eae5a9aac67d38c11ea),:760,2349-2357,3218-3227,3446-3450. These establish data-directory resolution, not descriptor-path behavior.0.10.19, ai-memory's locked crates.io release,src/providers/env.rs:504-519,614-625andsrc/value/parse.rs:99-105; retained provenance is indocs/decisions/2026-09-29-key-management.md:1256-1265atac01e51b.tools/muse_file_fence.py; the earlier folds found no upstream sign-in-path source. This fold adds no stronger claim.Evidence-class table
d_path; the suffix is ambiguoussource_reviewsource_reviewlocal_integrationac01e51b, ignoring docstrings, exit 0local_integration/source_reviewf2462c37, exit 0; final-head manifest count and comparison abovesynthetictests/test_canary_proof.py:967-997rerun, exit 0source_reviewtools/credentials/canary_scan_worker.py:418andWalk.traverse; no new runtime claimLocal commands recorded for Fold 9
The Python commands used the preinstalled CI virtual environment on
PATH. The manifest counts below describe the finalf2462c37head; the other results are retained Fold 9 checks.Fold 9's worker AST comparison also exited 0; no test file changed. Its kernel and manual pins were verified locally. Fold 9 did not repeat the earlier folds' full canary, mutation or guard acceptance runs, or qualify a host installation.
Local commands run for Fold 10
Python commands used
PATH=/var/tmp/cc-cc-work/venv-ci/bin:$PATHfrom the workspace root.The pinned kernel and manual file comparisons were rerun with exit 0 for each; their tags resolve to the full commits above. The worker's executable AST still equals
ac01e51bafter removing docstrings. A diff againstf2462c37confirms unchanged worker bytes and tests. Final-head manifest inspection confirms 12,307 entries, no PR-body row and exactly the decision and worker rows changed fromac01e51b; only the decision row changes in this correction fold. These checks do not reproduce historical fail-first, mutation, full ST9 acceptance or host-installation claims. No new behavioral regression is required for the two documentation corrections.Decision record
docs/decisions/2026-09-29-key-management.md:1314records NC-1240-1's mechanism, ambiguity policy, exact citations, verification and bounded layer-resolution scope. Its Fold 10 addendum at:1392records R-1240-1 and R-1240-2, the pinned final-head evidence and the dispositions of NC-1240-1, R-1240-3 and F3. The previous template-count addendum remains indocs/decisions/2026-10-02-new-wsl-client-configuration.md.Boundaries and follow-ups
The proof remains Linux-only; a macOS descriptor-path branch is not implemented. The existing tests cover directory, glob and file roots; git and tasks roots use the same code and remain separately untested. The earlier class-wide follow-up retains read-class and spelling gaps, other profile exclusions, alternate data-directory sources, prepare-time binding of overrides and whole-store archive/interpreter handling. A value exported only in the writer's shell remains outside the proof's binding. Host installation and the subsequent host-copy test belong to the existing landing procedure.
Checklist
scripts/host_receipts.py'sregister_fileAPI.🤖 Generated with Claude Code