Shadow Git Design
Auto-track edits made via moss edit for undo/redo capability.
Problem
When moss edit modifies files, there's no easy way to undo changes. Users must rely on git or manual backups.
Solution
Maintain a hidden git repository (.moss/shadow/) that automatically commits after each moss edit operation, preserving full edit history as a tree.
Why Shadow Git?
Primary goal: prevent catastrophic loss. LLM-driven edits can be unpredictable. A single bad edit or a chain of "fixes" can destroy working code. Shadow git provides a safety net that doesn't rely on user discipline.
Use cases:
- Oops recovery: "That delete was wrong, undo it"
- Experiment freely: Try aggressive refactors knowing you can always go back
- Audit trail: See exactly what moss changed, when, and why
- Checkpoint comparison: "What did moss do since my last git commit?"
- Partial rollback: Undo specific hunks while keeping others
Philosophy:
- Never destroy history - even undo preserves the undone state as a branch
- Err on the side of keeping data - disk is cheap, lost work is expensive
- Shadow is invisible until needed - zero friction for normal workflow
- Real git is source of truth - shadow serves the gap between edits and commits
- Minimize friction, maximize affordances - prompts default to the common/safe choice (e.g., global prune defaults to yes for security)
- Non-interactive fallbacks - every interactive prompt has a flag equivalent for LLM/scripted usage
Core Features
Automatic Tracking
- Every
moss editoperation creates a shadow commit - Workflow-driven edits (
moss @workflow) also tracked, with workflow name as context - Commit message includes: operation, target, timestamp, optional user message
- Only tracks files modified by moss, not external changes
Edit Messages
moss edit src/foo.rs/bar delete --message "Removing deprecated function"
moss edit src/foo.rs/bar delete --reason "Removing deprecated function" # aliasOptional --message (or --reason) flag attaches a description to the edit, displayed in history and undo output.
Undo/Redo
moss edit --undo # Revert last moss edit, prints what was undone
moss edit --undo 3 # Revert last 3 edits, prints summary of each
moss edit --undo --dry-run # Preview what would be undone
moss edit --redo # Re-apply last undone edit
moss edit --goto <ref> # Jump to specific commitHistory (read-only)
moss history # Show recent moss edits
moss history src/foo.rs # Show edits for specific file
moss history --all # Show full tree structure
moss history --json # Machine-readable output (for LLM/scripting)
moss history --status # Uncommitted shadow edits since last git commit
moss history --diff <ref> # Show what a commit changed
moss history --diff 2 # Diff for commit 2JSON output example (moss history --json):
{
"head": 3,
"checkpoint": "abc123",
"edits": [
{
"id": 3,
"operation": "insert",
"target": "src/foo.rs/new_fn",
"files": ["src/foo.rs"],
"message": null,
"workflow": null,
"git_head": "abc123",
"timestamp": "2025-01-15T10:30:00Z"
},
{
"id": 2,
"operation": "rename",
"target": "src/foo.rs/helper",
"files": ["src/foo.rs"],
"message": null,
"workflow": null,
"git_head": "def456",
"timestamp": "2025-01-15T10:25:00Z"
}
]
}Flat structure for script/CI use. Tree structure (branches) visible via moss history --all text output; reconstruct from edit order if needed programmatically.
Note: moss history is the primary interface for shadow git. Mutations (--undo, --redo, --goto) work on both moss history and moss edit for convenience.
Undo output includes:
- Files changed
- Edit descriptions (from
--messageif provided) - Operation type and target
Configuration
[shadow]
enabled = true # Default: true
warn_on_delete = true # Confirm before deleting symbolsNote: No automatic retention/cleanup. History is permanent unless explicitly pruned. Setting enabled = false stops new tracking but preserves existing shadow history.
Architecture
Tree Structure (Not Linear)
Shadow history is a tree, not a linear history:
- Undo moves HEAD backward but doesn't destroy commits
- New edits after undo create a branch (fork in history)
- All edits preserved (can return to any previous state)
- Branches can be pruned for security (remove sensitive content from history)
A -- B -- C -- D (original history, still exists)
\
E -- F (new branch: after undoing to B, made edits E, F)
^
HEADUndo/redo mechanics:
--undo: moves HEAD to parent commit, applies reverse patch to user files--undo N: undoes N commits in sequence--redo: moves HEAD to child commit, applies forward patch- If multiple children exist (branch point), prompts user or requires
--redo <ref>
- If multiple children exist (branch point), prompts user or requires
- After undo, new edits create a branch from current HEAD
- Original commits (like D above) still exist, reachable via
--history --all
Conflict handling: If reverse patch doesn't apply (file was modified externally):
- Abort undo and report conflict
- User must resolve manually (e.g., discard external changes or use
moss edit --force-undo) --force-undooverwrites file with shadow's known state (destructive)
Branch navigation: To restore a different branch:
moss edit --goto <ref> # Move HEAD to ref, restore file to that state
moss edit --goto 2 # Go to commit 2 (by number from --history)Directory Structure
.moss/
shadow/
.git/ # Shadow repository
refs/
heads/
main # Current position in edit tree
worktree/ # Working copy of tracked filesThe shadow repo tracks files in a separate worktree, not the user's actual files. On each moss edit:
- Copy current file state to worktree (captures "before")
- Apply edit to user's file
- Copy new file state to worktree (captures "after")
- Commit the change
Initialization: Shadow git is created on first moss edit (if [shadow] enabled = true, which is the default). The "initial state" commit (commit 0) captures the file's state before that edit. No shadow repo exists until edits are made.
Shadow Commit Format
Commit message (structured for parsing):
moss edit: delete src/foo.rs/deprecated_fn
Message: Removing deprecated function
Operation: delete
Target: src/foo.rs/deprecated_fn
Files: src/foo.rs
Git-HEAD: abc123For workflow-driven edits:
moss edit: insert src/foo.rs/new_handler
Workflow: @api-scaffold
Operation: insert
Target: src/foo.rs/new_handler
Files: src/foo.rs
Git-HEAD: abc123Git stores the diff separately. Timestamp comes from git commit metadata. Git-HEAD records the real git commit at time of edit (for checkpoint detection).
Undo Granularity
Git's patch APIs enable fine-grained undo:
--undoreverts entire commit (all files, all changes)--undo --file src/foo.rsreverts only that file from the commit
Each partial undo creates a new shadow commit with just those reversals.
Multi-File Edits
Some operations may touch multiple files (future: cross-file refactors like moss move):
- Shadow commit is atomic: all files in one commit
- Partial undo (file level) available via
--fileflag --history src/foo.rsfilters to show only commits affecting that file
Branch Pruning (Security)
If sensitive content was accidentally committed:
moss edit --prune <commit-range> # Remove commits from shadow history
moss edit --prune-file src/secrets.rs # Remove all history for a fileUses git filter-branch or similar under the hood. Important for:
- Removing accidentally committed secrets
- Cleaning up after experiments
- Reducing repo size
Design Decisions
D1: Tree structure over linear
- Decision: Preserve all history as tree
- Rationale: Undo shouldn't destroy information; users might want to return to undone state
- Trade-off: More disk usage, but git handles this well
D2: Per-file filtering (not branches)
- Decision: Single unified timeline, with per-file filtering via
--history <file> - Rationale: Per-file branches create confusing parallel timelines. One chronological history is simpler.
- Implementation:
--history src/foo.rsfilters to commits affecting that file, but all commits share one timeline
D3: Storage format
- Decision: Use git
- Rationale: Delta compression, familiar tooling, handles trees naturally
D4: External changes
- Decision: Re-sync by reading current file state before commit
- Rationale: Shadow tracks moss edits, not manual edits; patch may fail if file diverged
D5: Relationship to real git
- Decision: Real git is source of truth; shadow tracks uncommitted moss edits
- Rationale: Once user commits in real git, they've accepted those changes. Shadow serves the gap between edits and commits.
- Mechanics:
- Shadow records real git HEAD at each shadow commit (for context)
- On each
moss edit, check if real git HEAD changed since last shadow commit → checkpoint --undoby default won't cross checkpoint boundaries (user explicitly committed)--undo --cross-checkpointallows undoing past a real commit (with warning)moss edit --statusshows: shadow edits since last real commit
- Git operations that change files (
git reset,git checkout,git stash pop):- Detected on next
moss editvia HEAD or file content mismatch - Shadow re-syncs: records new file state as baseline, creates checkpoint
- Old shadow history preserved but marked as pre-divergence
- Detected on next
- Decision: Keep old shadow history after checkpoint (archaeology). Disk is cheap, lost work is expensive. Manual
--pruneavailable if needed.
D6: Multiple worktrees
- Problem: User may have multiple git worktrees of the same repo. Each worktree has its own file state.
- Decision: Each worktree gets its own shadow repo, sharing nothing
- Rationale: Shadow tracks file state, which differs per worktree. Config consistency with current worktree state is cleaner.
- Implementation: Shadow repo at
.moss/shadow/within each worktree's directory - Pruning across worktrees:
--prunedetects if same file has shadow history in other worktrees- Interactive: prompts "Also prune in N other worktrees? [Y/n]" (default: yes)
--prune --all-worktreesnon-interactive, prunes everywhere--prune --localnon-interactive, prunes only current worktree
Implementation Plan
Phase 1: Basic Infrastructure
- [ ] Create
.moss/shadow/git repo on firstmoss edit - [ ] Commit file state before each edit
- [ ]
--message/--reasonflag for edit descriptions - [ ]
moss historycommand (list recent edits) - [ ]
moss history --jsonfor machine-readable output - [ ]
moss history --diff <ref>to view changes
Phase 2: Undo/Redo + Git Integration
- [x]
moss edit --undoapplies reverse patch, moves HEAD backward - [x]
moss edit --undo Nreverts N edits in sequence - [x]
moss edit --undo --dry-runpreview without applying - [x]
moss edit --undo --filepartial undo for specific file - [x]
moss edit --redomoves HEAD forward - [x]
moss edit --goto <ref>jumps to arbitrary commit - [x] Conflict detection and
--forcefor external modifications - [x]
moss history --allshows full tree structure - [x]
moss history <file>filters to commits affecting that file - [x]
moss history --statusshows uncommitted shadow edits - [x] Checkpoint integration: record real git HEAD, respect commit boundaries
--undorefuses to cross git commit boundaries by default--cross-checkpointallows undoing past real git commits
Phase 3: Security + Polish
- [x]
moss history --prune Nfor removing old commits (keep last N) - [x]
warn_on_deleteconfirmation in config (requires --yes/-y)
Risks
Disk usage: Tree structure preserves everything
- Mitigation:
--prunefor manual cleanup,git gccompresses objects. Most edits are small diffs.
- Mitigation:
Performance: Git operations add latency
- Mitigation: Commits are small; consider async commits for non-blocking edits
Complexity: Tree navigation
- Mitigation: Simple undo/redo for common case; tree visible only via
--history --all
- Mitigation: Simple undo/redo for common case; tree visible only via
Example Session
Basic undo/redo
$ moss edit src/foo.rs/old_fn delete --message "Cleanup"
delete: old_fn in src/foo.rs
$ moss edit src/foo.rs/helper rename new_helper
rename: helper -> new_helper in src/foo.rs
$ moss history
2. [HEAD] rename: helper -> new_helper in src/foo.rs
1. delete: old_fn in src/foo.rs "Cleanup"
$ moss edit --undo 2
Undoing 2 edits:
[2] rename: helper -> new_helper
[1] delete: old_fn "Cleanup"
Files restored: src/foo.rs
HEAD now at: (initial state)
$ moss edit src/foo.rs/new_fn insert "fn new_fn() {}"
insert: new_fn in src/foo.rs
(created branch from initial state)
$ moss history --all
* 3. [HEAD] insert: new_fn in src/foo.rs
|
| 2. rename: helper -> new_helper in src/foo.rs
| 1. delete: old_fn in src/foo.rs "Cleanup"
|/
0. (initial state)Checkpoint behavior (real git integration)
$ moss edit src/foo.rs/bar delete
delete: bar in src/foo.rs
$ git add -A && git commit -m "Remove bar"
[main abc123] Remove bar
$ moss edit src/foo.rs/baz delete
delete: baz in src/foo.rs
(checkpoint: git commit abc123)
$ moss edit --undo
Undoing: delete baz in src/foo.rs
Files restored: src/foo.rs
$ moss edit --undo
error: Cannot undo past checkpoint (git commit abc123).
hint: Use --undo --cross-checkpoint to undo past real git commits.
$ moss history --status
Shadow edits since last commit: 0
Last checkpoint: abc123 "Remove bar"