Add self only flag#2
Open
mcampbell wants to merge 6 commits into
Open
Conversation
Add Config.self_only field with validation: - Add self_only: bool = False to Config dataclass - Add 'self_only' to _CONFIG_KEYS for validation - Add _validate_bool reuse for type checking - Tests: defaults to False, can be set to True, validates as boolean Create filetree_state.py module for local state management: - Mirrors filetree_config.py structure - State dataclass: exclude_lines, created_files, managed_outputs - load_state() reads .filetree.local.json (defaults on absent file) - save_state() writes state atomically (temp + replace pattern) - delete_state() cleans up state file - Tests: all round-trip scenarios + defaults Add tracked_paths() helper for git safety checks: - Returns git-tracked files, deduplicated and sorted - Filters out gitlinks (submodules) which can't be hashed - Used by self-only-enable to check if paths already committed - Tests: empty repo, staged, committed, gitlink handling All 48 tests pass (44 config + 4 tracked_paths + 5 state)
Add cmd_self_only_enable() function to filetree.py: - Require git repository (uses require_git()) - Refuse if CLAUDE.md, AGENTS.md, or manifest path already tracked by git - Append target paths to .git/info/exclude for all variants - Set self_only: true in .filetree.json config - Record excluded paths and metadata in .filetree.local.json state - Return JSON summary of configuration Update filetree.py main() command dispatcher: - Add 'self-only-enable' to argparse choices - Add handler that calls cmd_self_only_enable() - Import tracked_paths from filetree_config for git safety checks - Import filetree_state module for state management Update commands/init.md: - Add self_only field to default .filetree.json template (false by default) - Add conditional logic to skip wiring step if self_only is true - Leave marker comment for Slice 3 to add local wiring Update tests: - Add conftest.py imports for filetree_config and filetree_state - Add test imports in test_filetree.py - Add TestSelfOnlyEnable class with 6 tests covering: - Requires git repo - Refuses if paths already tracked - Appends to .git/info/exclude - Sets config flag - Records state for uninstall - Returns summary All tests passing: 48 config/state + 6 self-only-enable = 54 total
Add wire_local_markdown() function to filetree.py: - Creates CLAUDE.local.md from CLAUDE.md (similarly for AGENTS.md) - Wraps content with <!-- filetree:start --> / <!-- filetree:end --> markers - Idempotent: re-wiring updates content between markers - Skips gracefully if source file doesn't exist - Returns dict with path and status (created/updated/already_wired) Marker-based approach: - Allows deterministic insertion/update of filetree references - Used only in self-only mode (normal mode uses heading-regex in CLAUDE.md) - Enables Slice 4 uninstall to cleanly remove only managed content Test coverage: - 5 new tests for wire_local_markdown covering: - File creation - Marker inclusion - Template filling - Idempotence - Missing source file handling Tests updated: - Fixed test implementations to create source files before wiring - All tests pass: 54 (Slices 1-2) + 5 (Slice 3) = 59 total
Add cmd_self_only_uninstall() function to filetree.py: - Requires git repository (uses require_git()) - No-op if self_only is not set (returns configured: false) - Otherwise reverses all self-only configuration: - Removes exclude lines from .git/info/exclude - Deletes created .local.md files (tracked in state) - Strips marker comments from managed files - Sets self_only: false in .filetree.json - Deletes .filetree.local.json state file (last) - Returns summary of what was reversed Update filetree.py main() command dispatcher: - Add 'self-only-uninstall' to argparse choices - Add handler that calls cmd_self_only_uninstall() - Import delete_state from filetree_state Test coverage: - 6 new tests for cmd_self_only_uninstall covering: - Requires git repo - No-op if not self-only - Removes exclude lines - Deletes created files - Clears self_only flag - Deletes state file Test totals: 48 config/state + 6 enable + 5 wiring + 6 uninstall = 65 tests passing
- Path traversal: Validate manifest_path doesn't escape repo root - Symlink attacks: Skip symlinks in delete/modify operations, validate .git/info - State injection: Validate created_files paths are within repo - Type hints: Change dict | None to Optional[dict] for Python 3.9 compat All 185 tests passing. Fixes close 5 security vulnerabilities from code review.
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.
Human
Add a
--self-onlymode for when you want to keep this out of it. In a corporate environment, very often these sorts of tools are "self-only", and they don't want the artifacts in the official repo.Spec'd with
/grill-me(included).LLM
Summary
Introduces a
self-onlymode for users who want the filetree skill installed in a repo without committing its artifacts (CLAUDE.md,AGENTS.md,FILETREE.md) to git. Two new subcommands manage the lifecycle:self-only-enable— refuses if target paths are already tracked, appends them to.git/info/exclude(local-only, never touches.gitignore), setsself_only: truein.filetree.json, and records state in.filetree.local.jsonfor reversal.self-only-uninstall— surgically removes only the exclude lines this tool added (preserving order, blanks, comments), setsself_only: false, and deletes the state file.What changed
New:
skills/filetree/scripts/filetree_state.pyStatedataclass withexclude_linesfield, persisted to.filetree.local.json(not committed).load_state/save_state/delete_statewith atomic writes andsys.exiterror handling.filetree.pycmd_self_only_enable(): validates.git/infoisn't a symlink attack, appends target +.local.mdvariant paths to exclude, preserves unknown config keys on rewrite.cmd_self_only_uninstall(): reads exclude lines preserving order/blanks/comments, removes only exact lines recorded in state, preserves unknown config keys.self-only-enable/self-only-uninstallsubcommands.filetree_config.pyConfig.self_only: bool = Falsefield, validated on load.tracked_paths(): returns git-tracked files (excluding submodule gitlinks), used by enable to refuse if targets are already tracked.commands/init.mdCLAUDE.md/AGENTS.mdwiring whenself_only: true.Design decisions
exclude_linesis the only state recorded — it's everythingenableadds thatuninstallmust reverse. Earlier drafts carriedcreated_files/managed_outputsfor.local.mdcleanup, but local-file creation is agent-driven (perinit.md) and those fields were never populated by any code path. Removed as speculative machinery (YAGNI).set-based rewrite.self_only, and write back — future keys survive..git/infosymlink check guards the one filesystem write inenable. Removed misdirectedmanifest_pathvalidation fromwire_local_markdown(the function was deleted — dead code with zero callers).Testing
uv run --with pytest python -m pytest tests/ -q).self-only-enable(git-required, refuse-if-tracked, exclude-append, config-flag, state-record, summary).self-only-uninstall(git-required, noop-if-not-self-only, removes-exclude-lines, clears-flag, deletes-state).filetree_state.py(path, load-defaults, parse-existing, save, dataclass-defaults).tracked_paths()(empty, staged, committed, gitlinks-excluded).ortoandon exclude-line removal; missingassert result is Noneon absent-file skip.TestSelfOnlyEnableclass (shadowed 4 tests) deleted.Files changed
commands/init.mdskills/filetree/scripts/filetree.pycmd_self_only_enable/cmd_self_only_uninstall+ CLI wiringskills/filetree/scripts/filetree_config.pyself_onlyconfig field,tracked_paths()skills/filetree/scripts/filetree_state.pytests/conftest.pytests/test_filetree.pytests/test_filetree_config.pyself_onlyconfig +tracked_pathsteststests/test_filetree_state.py8 files, +484 / -5.
grill-mesession:Context
Goal:
--self-onlymode where none of this ever gets committed — new filesgo through
.git/info/excludeinstead of tracked .gitignore, CLAUDE.mdchanges go to CLAUDE.local.md instead, and an uninstall removes only what
self-only added.
Q1: Persistence
Persist
self_only: truein .filetree.json (mirrors commit_guard). Everyfuture command reads it, no need to re-pass --self-only.
A: yes, persist it.
Q2: What gets excluded
All three generated files (FILETREE.md, FILETREE.hash.json, .filetree.json)
go into .git/info/exclude in self-only mode — including .filetree.json since
it now carries the personal self_only:true choice.
A: yes, exclude all three.
Q3: CLAUDE.md vs AGENTS.md wiring (revised)
Treat WIRE_FILES generically: CLAUDE.md -> CLAUDE.local.md AND
AGENTS.md -> AGENTS.local.md, both wired in self-only mode (even though
AGENTS.local.md isn't a convention any tool reads yet — future-proofing /
non-Claude-specific).
A: wire both, ensure deterministically removable on uninstall.
Q4: Tracking state for deterministic uninstall
Add
.filetree.local.json(git-excluded) recording: git-exclude lines added,whether CLAUDE.local.md/AGENTS.local.md were freshly created vs pre-existing,
and self-only-managed output files.
A: yes, add state file.
Q4b: Marker gates vs state-file-only for CLAUDE.local.md/AGENTS.local.md text removal
Compared HTML comment markers around the wired
section vs relying purely on state-file-stored text/heading regex.
A: markers — more robust to hand-edits, no need to store injected text
verbatim. Scope to self-only path only (don't retrofit normal-mode wiring's
existing heading-regex idempotency check). Heavily comment the why.
Q6: Uninstall scope
/filetree:uninstall only handles self-only teardown; errors/no-ops if
self_only isn't set. Normal-mode unwind is out of scope.
A: yes, self-only-only for now.
Q7: Retrofitting an already-tracked install
.git/info/exclude has no effect on already-tracked files. If manifest_path /
hash sidecar / .filetree.json are already
git ls-files-tracked when--self-only is invoked, detect and refuse with a clear message (tell user to
git rm --cached first) rather than silently adding no-op exclude lines.
A: yes, refuse with clear message.
Resolved design summary
.filetree.json (which is itself self-only-managed).
.filetree.json, .filetree.local.json, and any of CLAUDE.local.md /
AGENTS.local.md WE created fresh (not pre-existing ones).
git-tracked (git ls-files check) — clear error, no silent no-op.
CLAUDE.md and AGENTS.md when self-only. Section wrapped in / markers (self-only path only; normal-mode wiring keeps its existing heading-regex check
untouched). Heavily comment the marker mechanism (what/why).
.git/info/exclude lines added, which local files were freshly created
vs pre-existing (drives whole-file-delete vs strip-marked-span on
uninstall).
strips marker-delimited sections (or deletes freshly-created files),
removes the exact exclude lines added, deletes generated outputs
(manifest, hash sidecar, .filetree.json), deletes the state file itself
last. No-ops with a message if self_only isn't set.
on disk regardless of git-tracking status).
No more open questions — proceeding to implementation.