docs: document project root, apply-zip review, snapshots and CONTEXTZIP.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
akadeepesh
2026-10-19 16:40:12 +05:30
co-authored by Claude Opus 5.5
parent 5eb6644027
commit e14c3d569d
3 changed files with 96 additions and 21 deletions
+70 -10
View File
@@ -46,9 +46,23 @@ These flags work on the main command **and** on `include` and `exclude`. Flags g
| `--no-gitignore` | | Ignore the project's `.gitignore` (use only the built-in rules). |
| `--git-changes` | | Only include files git reports as modified, added or untracked. The project must be inside a git repository. |
| `--verbose` | `-v` | Show every included and excluded file. |
| `--here` | | Only package the folder you're in. Paths in the ZIP stay relative to the project root. |
`--include` and `--exclude` (and the `include` / `exclude` subcommands) also work together with `--git-changes`, and are combined with `always_include` / `always_exclude` from `.contextzip/config.json`.
## Project root
Every command works from the **project root**, wherever you run it from, like git does. Run `cz` inside `frontend/` of a monorepo and the workspace stays at the root (no `frontend/.contextzip/`), the ZIP's paths are root-relative, and `apply-zip` puts files back in the right place.
The root is, first match wins:
1. `CONTEXTZIP_ROOT`, if set.
2. The **outermost** folder (not above your home folder) that is a git repository (`.git` folder or file) or has a `.contextzip/config.json`. Outermost on purpose: tools like create-next-app run `git init` inside the folder they generate, which leaves a nested `frontend/.git` in your monorepo.
3. The outermost folder with an existing `.contextzip/` workspace.
4. The current folder.
Paths you type are relative to where you are, like `git add`: from `frontend/`, `cz include src/` means `frontend/src/`. Add `--here` to package just the current folder. `contextzip config` shows which root was picked and why. `--git-changes` also reads repositories nested inside the root (an embedded `frontend/.git`, or submodules).
## `contextzip`
Run from your project root. contextzip detects your stack, applies smart exclusions and writes a ZIP into `.contextzip/`. When it finishes, the ZIP is placed on your clipboard as a real file, so you can paste it straight into Claude, ChatGPT or any AI tool's upload box. If the clipboard can't be used, it opens the folder instead and tells you why.
@@ -99,25 +113,56 @@ Quote patterns that contain `*` so your shell doesn't expand them first.
Apply an AI-returned ZIP back into your project.
```bash
contextzip apply-zip [ZIP] [--manifest PATH] [--dry-run] [--verbose] [--yes]
contextzip apply-zip [ZIP] [--manifest PATH] [-n] [-v] [--review | --editor | --safe-only | -y]
```
| Argument / flag | Short | Description |
|---|---|---|
| `ZIP` | | Path to the returned ZIP. Optional: if omitted, contextzip looks in `.contextzip/inbox/`. An explicit path overrides the inbox. |
| `--manifest PATH` | | Manifest to compare against, instead of auto-detecting the most recent one in `.contextzip/output/`. |
| `--dry-run` | `-n` | Preview what would change without writing anything. |
| `--verbose` | `-v` | Show every file and its status, not just the summary. |
| `--yes` | `-y` | Skip the confirmation prompt, even for risky changes. |
| `--manifest PATH` | | Snapshot to compare against, instead of the one that best matches the ZIP. |
| `--dry-run` | `-n` | Show what would change; write nothing. |
| `--verbose` | `-v` | List every file and what will happen to it. |
| `--review` | | Decide file by file in a local browser page with side-by-side diffs. |
| `--editor` | | Resolve files that need a decision in VS Code (or Cursor, Windsurf, VSCodium). |
| `--safe-only` | | Apply only the safe changes; leave the rest untouched. For scripts and CI. |
| `--yes` | `-y` | Apply everything without asking. Where you and the AI disagree, the AI's version wins. Never removes files. |
| `--snapshot ID` | | The snapshot this reply answers (e.g. `14`). Usually found automatically. |
| `--remove-moved` | | Remove the old copy of files the AI moved (only if you haven't changed them). |
| `--allow-delete` | | Delete files the AI asked to delete in `CONTEXTZIP.md` (only if you haven't changed them). |
```bash
contextzip apply-zip # auto-detect from .contextzip/inbox/
contextzip apply-zip fix.zip # explicit path
contextzip apply-zip --dry-run --verbose
contextzip apply-zip -y # no confirmation, even if risky
contextzip apply-zip # from .contextzip/inbox/, asks when something needs a decision
contextzip apply-zip fix.zip # a ZIP anywhere on disk
contextzip apply-zip --dry-run -v # see exactly what would happen
contextzip apply-zip --review # decide in the browser
contextzip apply-zip --editor # three-way merge in VS Code
contextzip apply-zip --safe-only # CI: only the safe changes
```
**How it works.** Every ZIP contextzip creates gets a local manifest — a hash of each included file — written next to it in `.contextzip/output/`. It is never inside the ZIP, so it is never uploaded and never visible to the AI tool. `apply-zip` compares the returned ZIP against that manifest and classifies every file as new, modified, unchanged, or needing a closer look (edited locally since zipping, or no baseline at all) before writing anything. It only adds and modifies files — **nothing is ever deleted**.
**Where each file goes.** The returned ZIP doesn't need to match your layout. Each file is matched to the project by its longest shared path suffix, so a ZIP wrapped in an extra folder, made relative to a subfolder (`src/...` for `frontend/src/...`), flattened to bare names, or using backslash separators still lands correctly. The layout learned from files that clearly match is applied to brand-new files too. A name that fits several files is settled by content; if it's still unclear the file is marked *ambiguous* and never written on a guess. `__MACOSX/`, `.DS_Store` and contextzip's own `prompt.txt` are ignored.
**What it compares against.** Every ZIP contextzip makes gets a local manifest, never uploaded. `apply-zip` picks the manifest that best matches the returned ZIP, not just the newest, so an unrelated run in between (or `-o`) doesn't confuse it. Each file is then compared three ways: what you sent, what's on disk now, and what came back.
| Status | Meaning | Default |
|---|---|---|
| new | The file doesn't exist yet | written |
| modified | Only the AI changed it | written |
| merged | You both changed it in different places — three-way merged | written |
| unchanged | Already matches, including "you already applied this" | skipped |
| conflict | You both changed the same lines | needs your call |
| drifted | You changed it and it can't be merged (binary, or the original ZIP is gone) | needs your call |
| deleted | You deleted it after zipping | needs your call |
| untracked | Exists here but wasn't in the ZIP you sent | needs your call |
| partial | The AI returned a cut-down file (`// ... rest unchanged`) | needs your call |
| ambiguous | Can't tell which file it belongs to | needs your call |
| moved | The AI moved or renamed it (declared in `CONTEXTZIP.md`, or near-identical content) | new path written; old file kept unless you say so |
| delete-requested | The AI asked to delete it in `CONTEXTZIP.md` | needs its own yes |
**Snapshots.** The snapshot is found from the `CONTEXTZIP.md` the reply echoes back, or, if it didn't, by matching content against your snapshots. Since every snapshot keeps a copy of what was sent, merging works even if you've packaged again since.
Line endings never count as a change, and text is written back with the local file's line endings and BOM. Secret files that were redacted on the way out are merged against the redacted copy, so your real values are kept and a `[REDACTED]` placeholder is never written over them.
When something needs a decision, `apply-zip` asks: apply the safe changes and leave the rest, review in the browser, resolve in your editor, apply everything, or quit. Without a terminal to ask (CI), it writes nothing unless you pass `--safe-only` or `--yes`. If anything is left undecided, the ZIP stays in the inbox so you can run `apply-zip` again. Only adds and modifies files — **nothing is ever deleted**.
## `contextzip watch`
@@ -174,6 +219,20 @@ contextzip config --ui # visually set include/exclude
A workspace location can also be pinned for a whole team by committing `.contextzip/config.json` at the project root, for example `{"workspace_location": "git-root"}`. A project config takes priority over your personal setting.
## `contextzip snapshots`
List this project's snapshots: number, token, mode, file count, age, and whether a reply has been applied.
```bash
contextzip snapshots
```
Every package is kept as a numbered snapshot in `.contextzip/snapshots/` (local, never uploaded): a copy of the ZIP plus a manifest of what was sent. Each ZIP's `CONTEXTZIP.md` names its snapshot, e.g. `Snapshot: 14-k3f9a2`. Snapshots are kept while they're among the newest `snapshots.keep` (10), younger than `snapshots.max_age_days` (14), or still awaited by a reply in the inbox.
## `CONTEXTZIP.md`
A short file added to every ZIP for the AI that receives it: the project, the snapshot ID, and how to reply so `apply-zip` can put the answer back cleanly — only changed files, complete contents, the same paths, `[REDACTED]` values left alone, and moves or deletions listed under **Changes** (`moved: a -> b`, `deleted: x`). Turn it off per project with `"zip_guide": false` or in `contextzip config --ui`.
## `contextzip update`
Check PyPI for a newer release and update in place.
@@ -211,5 +270,6 @@ It detects whether contextzip was installed with pip, pipx or uv tool and re-run
| `.contextzip/output/watch/` | Debug packages from `contextzip watch` |
| `.contextzip/inbox/` | Where `apply-zip` looks for a returned ZIP when you don't pass a path |
| `.contextzip/config.json` | Project config: `always_include`, `always_exclude`, `workspace_location`, `ai`, size limits |
| `.contextzip/snapshots/` | Numbered snapshots: a copy of each ZIP you sent and its manifest — what `apply-zip` merges against |
Where `.contextzip/` itself lives defaults to the git root; see `contextzip config --set-workspace`.
+21 -9
View File
@@ -91,6 +91,7 @@ contextzip --git-changes # only what you've changed
contextzip --prompt "fix the login redirect bug" # let AI pick the relevant files
contextzip include src/ app/ # only these folders
contextzip include src/ .env # .env too, values redacted
contextzip --here # only the folder you're in (monorepos)
contextzip exclude tests/ '*.snap' # everything except these
contextzip apply-zip # write the AI's changes back
```
@@ -145,17 +146,25 @@ contextzip apply-zip fix.zip # or point at one directly
contextzip apply-zip --dry-run # preview without writing anything
```
Every ZIP contextzip creates gets a **local manifest** containing a hash of each file. It's stored next to the ZIP and never inside it, so the AI tool never sees it. When a ZIP comes back, `apply-zip` compares every file against that manifest:
AI tools return ZIPs in every shape: wrapped in an extra folder, relative to a subfolder, flattened, with Windows separators. `apply-zip` matches each file to its real place in your project anyway, and new files follow their siblings.
| Status | Meaning | Action |
Every ZIP contextzip makes gets a **local snapshot** (a hash of each file, never uploaded). `apply-zip` picks the snapshot that best matches the returned ZIP and compares three versions of each file: what you sent, what's on disk now, and what came back.
| Status | Meaning | Default |
|---|---|---|
| **New** | File didn't exist before | Written |
| **Modified** | AI changed it, and you haven't touched it locally | Written |
| **Unchanged** | Identical to what's on disk | Skipped |
| **Drifted** | *You* edited it locally since zipping | ⚠️ Asks before overwriting |
| **Untracked** | Exists locally but wasn't in the original ZIP | ⚠️ Asks before overwriting |
| **New** / **Modified** | New file, or only the AI changed it | Written |
| **Merged** | You both changed it, in different places | Written (three-way merge) |
| **Unchanged** | Already matches — including changes you already applied | Skipped |
| **Conflict** | You both changed the same lines | Needs your call |
| **Partial** | The AI returned a cut-down file (`// ... rest unchanged`) | Needs your call |
| **Moved** | The AI moved or renamed it | Written at the new path; the old file is kept unless you say otherwise |
| **Drifted** / **Deleted** / **Untracked** / **Ambiguous** / **Delete requested** | Can't be applied safely without you | Needs your call |
`apply-zip` only adds and modifies files. **It never deletes anything.** It also unwraps the extra top-level folder that AI tools and "Download ZIP" buttons often add.
When something needs a decision you choose: apply the safe changes now, **review side by side in your browser** (`--review`), **resolve in VS Code's merge editor** (`--editor`), or apply everything. Line endings never count as a change, redacted secrets are never overwritten with placeholders, and **files are only removed with your explicit OK** — never one you've changed since.
### Snapshots and CONTEXTZIP.md
Every package is kept locally as a numbered **snapshot** (`#14`), so a reply can be merged against exactly what you sent, even after you've packaged again. Each ZIP also carries a short **`CONTEXTZIP.md`** for the AI: the snapshot ID, and how to reply so the answer applies cleanly (complete files, same paths, secrets left as `[REDACTED]`, moves and deletions listed). When the reply echoes it back, `apply-zip` knows the exact snapshot. `contextzip snapshots` lists them; turn the file off with `"zip_guide": false`.
### Terminal error watcher
@@ -237,6 +246,8 @@ This holds per-project settings. Edit it by hand or with `contextzip config --ui
},
"applied_zip_retention": 1, // past apply-zip archives to keep
"cleanup": { "enabled": true, "keep_recent": 1 },
"snapshots": { "keep": 10, "max_age_days": 14 }, // history apply-zip merges against
"zip_guide": true, // add CONTEXTZIP.md to every ZIP
"webui": { "auto_open": true, "port": null }
}
```
@@ -299,11 +310,12 @@ contextzip keeps everything in a `.contextzip/` folder at your git root (change
│ ├── git-changes/ changes.zip + manifest + report (--git-changes)
│ ├── prompt/ vibe.zip + manifest + report (--prompt)
│ └── watch/ debug-context.zip (contextzip watch)
├── snapshots/ numbered copies of what you sent (contextzip snapshots)
├── inbox/ drop AI-returned ZIPs here for apply-zip
└── config.json project config (optional)
```
Old outputs are cleaned up automatically, and only the most recent set is kept. You can change this with the `cleanup` setting.
Old outputs are cleaned up automatically: `output/` keeps the latest ZIP per mode (`cleanup`), and `snapshots/` keeps the newest 10 or anything under 14 days old (`snapshots`) — never one a reply in the inbox is waiting on.
## Python API
+5 -2
View File
@@ -58,8 +58,11 @@ Every run writes a report next to the ZIP listing what was redacted and what was
- **Path handling.** Files outside the project directory are never included, and dangling or looping symlinks are skipped.
- **Local manifests.** The hashes `apply-zip` uses are stored next to the ZIP on your machine, never inside it.
- **`apply-zip` never deletes files** and asks before overwriting anything you've changed since the ZIP was made.
- **`apply-zip` removes files only with your explicit OK** — the old copy of a moved file, or a deletion the AI requested — never one you've changed since the snapshot, and never because of `--yes` alone. It asks before overwriting anything you've changed since the ZIP was made.
- **Snapshots stay local.** `.contextzip/snapshots/` keeps copies of the ZIPs you sent (already filtered and redacted, exactly as uploaded) so replies can be merged; it's gitignored and pruned automatically.
- **Config UI.** `contextzip config --ui` binds to `127.0.0.1` only, requires a random one-time session token on every request and shuts itself down after saving or after a period of inactivity.
- **Review page.** `contextzip apply-zip --review` uses the same model: `127.0.0.1` only, a random session token on every request, no outbound requests, and it shuts down when you apply, cancel or leave it idle. The page only sends your decisions back; the CLI does the writing.
- **Secrets on the way back.** Files that were redacted when packaged are merged against the redacted copy when an AI returns them, so `apply-zip` never writes a `[REDACTED]` placeholder over a real value. Entries in a returned ZIP that point outside the project (absolute paths, drive letters, `..`) make `apply-zip` refuse the whole ZIP.
- **API key storage.** Your Gemini key is stored in your personal config (`~/.config/contextzip/config.json`, or `%APPDATA%\contextzip\config.json` on Windows), never in the project. You can provide it with the `GEMINI_API_KEY` environment variable instead.
## Scope
@@ -69,7 +72,7 @@ In scope:
- secrets ending up in an archive despite the rules above,
- path traversal or symlink escapes when packaging or applying a ZIP,
- `apply-zip` writing outside the project directory,
- the config UI being reachable or controllable by another process or site,
- the config UI or review page being reachable or controllable by another process or site,
- network requests beyond those listed above.
Out of scope: what an AI tool does with an archive after you upload it, and secrets inside files you have chosen to include that the redaction patterns don't recognise. Please still report the second kind if you think a common format should be covered.