# Changelog All notable changes to contextzip are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project uses [Semantic Versioning](https://semver.org/). --- ## [0.1.1] — 2026-05-15 ### Fixed - LICENSE link in README now points to GitHub instead of a relative path that broke on PyPI - License metadata updated to SPDX string format (`"MIT"`) to comply with setuptools ≥ 77 --- ## [0.1.2] — 2026-05-15 ### Added - Smart framework and language detection (Next.js, Node.js, Python, Django, FastAPI, Rust, Go, Ruby) - Automatic exclusion rules per detected ecosystem - `.gitignore` patterns respected automatically (`--no-gitignore` to bypass) - `--include` flag for scoping to specific directories (exact path-boundary matching) - `--exclude` flag for additional gitignore-style exclusion patterns - `--dry-run` flag to preview without creating any files - `--verbose` flag showing every file with its size - `--output` flag for custom ZIP destination - `--no-clipboard` flag to skip clipboard/folder-open step - Tiered clipboard strategy: file-on-clipboard (macOS/Linux), Explorer with file selected (Windows), path fallback - Warnings for large files (≥ 1 MB), binary files, and skipped files (dangling symlinks, permission errors) - Rich terminal output with panels, progress bars, and colour-coded detection results - Compression stats (ratio, size before/after) shown after ZIP creation --- ## [0.2.0] — 2026-05-15 ### Added - `--git-changes` mode for packaging only files reported as changed by git - Automatic detection of: - staged files - unstaged modifications - untracked files - Dedicated git-aware packaging pipeline optimised for AI/code review workflows - Rich git change summary panel with staged/unstaged/untracked breakdown - Verbose git mode output showing categorized changed files - Graceful handling for: - repositories with no changes - deleted files - submodules - repositories outside the selected project tree - missing git installations - non-git directories - Support for monorepo-aware repository root resolution - Binary and large-file checks now also apply in git-changes mode ### Improved - File resolution architecture refactored to support multiple packaging strategies cleanly - Internal file scanning pipeline separated from git-based file selection logic - CLI output consistency improved between standard scan mode and git-changes mode ### Developer Notes - Introduced new `contextzip.git` module for git integration and porcelain parsing - Added `resolve_files_from_git()` pipeline for pre-selected file resolution - Added structured `GitChanges` and `GitError` models for cleaner error handling and extensibility --- ## [0.2.1] — 2026-05-16 ### Fixed - `--exclude` / `-e` patterns containing Windows-style paths (e.g. `.\CHANGELOG.md`) were silently ignored due to backslash and leading `.\` — patterns are now normalized to forward-slash posix paths before matching - Files passed to `-e` were not being excluded from the ZIP despite appearing in the exclusion list ### Improved - `-e` now accepts multiple space-separated patterns in a single flag invocation: `-e file1 file2 folder/` in addition to the existing repeatable form `-e file1 -e file2` - Folder exclusion via `-e folder/*` is now supported and normalized to `folder/` automatically ## [0.2.2] - 2026-05-16 ### Added - `contextzip exclude PATTERN…` subcommand — exclude multiple files and folders in a single invocation without repeating `-e` (e.g. `contextzip exclude CHANGELOG.md LICENSE .github/`) - `contextzip include PATH…` subcommand — symmetric counterpart to `exclude`; packages only the specified paths (e.g. `contextzip include src/ app/`) - All modifier flags (`--dry-run`, `--verbose`, `--output`, `--no-clipboard`, `--git-changes`, `--no-gitignore`) work on both subcommands ### Changed - `-e` / `--exclude` flag on the main command restored and kept for full backwards compatibility alongside the new subcommand - Warning messages for large files now mention both `-e PATTERN` and `contextzip exclude PATTERN` as remediation options - `_normalize_pattern` promoted to a named module-level function (testable, documented); behaviour is unchanged ### Fixed - Removed the previous positional-argument approach to exclusions on the main command, which was ambiguous and undiscoverable --- ## [0.2.3] — 2026-05-19 ### Added - Dedicated Ruby ecosystem exclusion rules via new `contextzip.rules.ruby` module - Automatic exclusion of common Ruby / Rails artifacts: - Bundler directories and lockfiles - Rails logs, temp files, assets, storage, and Spring cache - Built gems and compiled Ruby extensions - Coverage and generated documentation directories - Additional package discovery keywords for AI tooling and code packaging: - `claude` - `chatgpt` - `code-packaging` ### Changed - Ruby project detection now uses the dedicated Ruby rule module instead of the generic base rules - Git-based packaging now enforces base exclusion safety rules even for tracked files - Repository governance and metadata files are now excluded by default: - `CHANGELOG*` - `LICENSE*` - `CONTRIBUTING*` - `SECURITY.md` - `.github` issue / PR templates - `.gitignore` is no longer excluded automatically from packaged context archives ### Fixed - Prevented git-tracked sensitive files (e.g. `.env`, secrets, binaries) from being included in `--git-changes` mode - Improved ZIP packaging resilience when file stat operations fail during archive creation - Fixed progress tracking during ZIP generation to avoid repeated filesystem stat calls - Fixed Ruby framework detection routing so Ruby projects receive ecosystem-specific exclusion behaviour --- ## [0.2.4] — 2026-05-20 ### Added * Persistent `.contextzip/` workspace directory for generated context packages * Automatic output naming based on packaging mode: * `codebase.zip` for standard packaging * `changes.zip` for `--git-changes` * Automatic `.contextzip/` exclusion from packaged archives and file scanning * Automatic `.gitignore` integration: * `.contextzip/` is appended when a git repository is detected * missing entries are added only once * Repository-root-aware workspace placement: * `.contextzip/` is created at the git repository root when `.git/` is detected * falls back to the current working directory outside git repositories ### Changed * ZIP outputs are now written to the persistent `.contextzip/` workspace by default instead of temporary system directories * Generated archives now overwrite previous outputs with the same logical name instead of creating temporary randomized filenames * `--output` now bypasses all `.contextzip/` workspace logic and writes directly to the user-specified destination ### Improved * Generated context packages are now easier to rediscover, reuse, and inspect across AI-assisted development workflows * Workspace output structure now provides a foundation for future features such as: * timestamped package history * metadata indexing * incremental packaging * AI debug sessions * package caching ### Fixed * Prevented `.contextzip/` contents from recursively packaging previous generated archives * Improved fallback behaviour when workspace creation fails due to filesystem permissions or read-only directories ## [0.3.0] — 2026-05-24 ### Added * `--prompt` / `-p` flag for AI-powered file selection — describe your task in plain English and contextzip automatically selects the minimum relevant files (e.g. `contextzip --prompt "Change toast color on failed login"`) * Gemini 2.5 Flash Lite integration via a thin `httpx` client — no Google SDK required; a single lightweight POST call to the Gemini API * First-run onboarding flow for users without an API key: * Displays a guided panel with a link to [Google AI Studio](https://aistudio.google.com/apikey) (free, no credit card) * Optionally opens the browser automatically * Saves the key persistently on confirmation * Persistent API key storage in the platform-appropriate config directory: * `~/.config/contextzip/config.json` on Linux / macOS * `%APPDATA%\contextzip\config.json` on Windows * `GEMINI_API_KEY` environment variable always takes precedence * `contextzip config` subcommand for key management: * `contextzip config` — show current key status and source * `contextzip config --reset-key` — clear stored key and re-run onboarding * `contextzip config --show-key-path` — print config file location * `prompt.txt` is written as the first entry in every AI-generated ZIP, containing the task description, detected framework, and selected file list — so any AI tool receiving the archive immediately knows the context * Keyword-based heuristic file scorer as a rate-limit fallback: * Activates only on confirmed HTTP 429 from Gemini * Scores files by token overlap with the prompt, directory semantics, and extension type * User is warned clearly that heuristic was used and Gemini results are preferred * `--prompt --dry-run` combination — preview AI-selected files without creating a ZIP * API key validation on load — keys not starting with `AIza` are treated as unconfigured and trigger onboarding rather than a cryptic API error * `diagnose_api_key()` surfaces a human-readable explanation when a key exists but fails validation, before any API call is attempted ### Changed * `create_zip()` in `packager.py` now accepts an optional `prompt_txt` parameter; when provided, `prompt.txt` is written as the first ZIP entry * xdg-open on Linux now runs with stdout and stderr suppressed to prevent KDE / DBus / MIME warnings from appearing over the API key prompt ### Developer Notes * Introduced `contextzip.config` module for cross-platform config path resolution and key read / write / delete operations * Introduced `contextzip.ai` package with three modules: * `gemini.py` — Gemini API client with typed exception hierarchy (`GeminiError`, `GeminiRateLimitError`, `GeminiUnavailable`) * `heuristic.py` — keyword + directory scoring fallback scorer * `selector.py` — project map builder, AI orchestration, `prompt.txt` generation; returns `(paths, prompt_txt, method)` so callers know which selection path was taken * Added `httpx` as a required runtime dependency ## [0.3.1] — 2026-05-25 ### Added * `contextzip watch -- ` subcommand — wraps any dev server or build process, buffers its output, detects errors automatically, and packages a debug-ready ZIP in a single keypress * Supports `npm run dev`, `python manage.py runserver`, and any process that writes errors to stdout/stderr * On error detection, renders an inline prompt beneath the error output: `[D] package debug context [S] skip` * Pressing `D` writes `.contextzip/debug-context.zip` immediately; the wrapped process continues running uninterrupted * On `Ctrl+C`, if no errors were packaged during the session, a final prompt offers to capture the full session output as a fallback * `debug-context.zip` — flat ZIP structure produced by `watch`: * `prompt.txt` — auto-generated from detected framework, error type, and referenced files; no `--prompt` flag required from the user * `terminal-error.txt` — cleaned, noise-stripped error block and stack trace * `source-files.zip` — inner ZIP containing source files extracted from stack frames, with relative paths preserved * `contextzip/error_parser.py` — full terminal output processing pipeline: * `strip_ansi()` — removes all CSI, OSC, and Fe escape sequences and bare carriage returns from raw process output * `detect_error_block()` — backward scan through the rolling buffer to find the most recent error; walks back from terminal exception lines to capture the full block including the `Traceback` header * `extract_paths()` — runs per-framework path regexes on the error block, resolves relative and bare filenames, and filters out stdlib, venv, and `node_modules` paths * `strip_noise()` — removes known noise lines (request logs, startup banners, HMR chatter) and collapses blank line runs * `build_prompt_txt()` — generates structured, AI-ready `prompt.txt` content * `process_buffer()` — convenience function running the full pipeline in a single call * `contextzip/watcher.py` — process management and packaging: * Spawns child via `subprocess.Popen` with piped stdout/stderr; two daemon threads drain pipes concurrently into a `deque(maxlen=2000)` rolling buffer * Detection thread polls the buffer every 300ms using `detect_error_block()` * Error deduplication: MD5 of the first three non-blank lines of an error block; same signature within a session suppresses repeat prompts * Single-keypress D/S prompt via `termios`/`tty` raw mode on Unix and `msvcrt` on Windows, with an `input()` fallback for unusual environments * Child terminated cleanly on `Ctrl+C`: SIGTERM → 3-second grace period → SIGKILL * `contextzip/rules/errors/python.py` — error detection pattern sets for Python, Django, and FastAPI: * 18 error start patterns covering tracebacks, syntax errors, Django/DRF exceptions, uvicorn/gunicorn ERROR logs, and pytest failures * 4 stack frame path patterns * 21 noise patterns for Django startup, request logs, pytest collection output, and pip install chatter * `contextzip/rules/errors/node.py` — error detection pattern sets for Node.js, Next.js, and React: * 33 error start patterns covering JS runtime errors, Next.js error symbols (`✗`, `×`, `⨯`), webpack/bundler errors, npm errors, and unhandled promise rejections * 7 stack frame and import path patterns * 32 noise patterns for Next.js/Vite/CRA startup, HMR chatter, and request logs ### Changed * `cli.py` refactored from a single 1,100-line file into four focused modules with no behaviour changes: * `cli.py` — Click group, four subcommands, and `_run()`; routing only * `cli_display.py` — all Rich rendering helpers (`print_detection`, `print_scan_summary`, `print_file_warnings`, `print_package_result`, etc.) * `cli_ai.py` — AI selection orchestration and `normalize_pattern` * `cli_onboard.py` — Gemini API key onboarding flow and browser launch * `pyproject.toml` updated to include `contextzip.rules.errors` in package discovery ### Developer Notes * `contextzip/rules/errors/` follows the same pattern as `contextzip/rules/`: one file per ecosystem, each exporting `ERROR_START_PATTERNS`, `PATH_PATTERNS`, and `NOISE_PATTERNS` as lists of compiled `re.Pattern` objects — adding support for a new framework means adding one file * `_load_patterns()` in `error_parser.py` maps ecosystem names from `DetectionResult.ecosystems` to rule modules; unrecognised ecosystems fall back to loading both Python and Node patterns as a best-effort default * `cli_display.py` functions accept an optional `con: Console` parameter (defaulting to the module-level console) making every display function independently testable without monkey-patching * No new runtime dependencies introduced — `watch` uses only stdlib (`subprocess`, `threading`, `queue`, `termios`/`msvcrt`, `zipfile`) ## [0.3.2] — 2026-05-25 ### Added * **Python API** — contextzip is now usable as a library, not just a CLI tool: * `get_git_changes(path?)` — returns a `FileCollection` of git-modified, added, and untracked files; applies base safety rules (no secrets, no binaries) automatically * `get_files(path?, include?, exclude?, use_gitignore?)` — returns a `FileCollection` of all project files after applying ecosystem rules, `.gitignore`, and any extra exclusion patterns * `create_zip(collection, output?)` — writes a `FileCollection` to a ZIP archive and returns a `PackageResult`; output path is caller-controlled * `detect_ecosystem(path?)` — returns a `DetectionResult` with ecosystem names, display string, and confidence level * `FileCollection` — unified return type for `get_git_changes` and `get_files`; supports iteration, `len()`, and `bool()` directly; `.files` is always a plain `list[pathlib.Path]` usable without zipping * `PackageResult` re-exported from the top-level package * Exception hierarchy: `ContextzipError` (base), `NotARepositoryError`, `GitNotFoundError`, `GitCommandError`, `NoFilesError` * All API functions default `path` to `Path.cwd()`, matching CLI behaviour * `contextzip/api.py` — new module implementing the public API; no Click, no Rich output, no `SystemExit`; raises typed exceptions on failure * `contextzip/packager.py` — added `create_zip_silent()` and `_workspace_output_path_silent()`: same ZIP logic as the CLI path but without a `Console` dependency, used internally by the API ### Changed * `contextzip/__init__.py` — now exports the full public API (`get_git_changes`, `get_files`, `create_zip`, `detect_ecosystem`, `FileCollection`, `PackageResult`, and all exception types) so `from contextzip import ...` works without knowing internal module paths ### Fixed * `filters.py` — symlink guard inner `try` block repeated the identical `relative_to` call that already failed in the outer block; the fallback now correctly uses the pre-resolve (`original_path`) to compute the relative path for symlinks that point outside the project tree * `watcher.py` — `_drain_output_queue` silently consumed sentinel values (`raw=None`) with `continue`, making them invisible to `_check_reader_done`; sentinels are now put back into the queue so done flags are set correctly * `watcher.py` — `_read_key_windows` had no timeout and would spin forever in non-interactive terminals; now uses a 60-second `monotonic` deadline matching the existing Unix `select` timeout * `packager.py` — removed unused `_safe_name` helper * `ai/gemini.py` — corrected stale docstring (`gemini-2.0-flash-lite` → `gemini-2.5-flash-lite`) to match the actual `DEFAULT_MODEL` constant ## [0.3.3] — 2026-05-29 ### Fixed * `contextzip.ai` and all subpackages were missing from the published wheel due to an incomplete `include` allowlist in `pyproject.toml` — replaced with auto-discovery so all current and future subpackages are captured automatically ## [0.3.4] — 2026-06-21 ### Added * `contextzip eod` — builds a paste-ready end-of-day prompt from today's Claude/ChatGPT conversation plus whatever code changed, copied straight to the clipboard. Does no summarizing itself — that's left to whichever AI tool the prompt gets pasted into * `contextzip handoff` — same pipeline, aimed at continuing a project in a fresh chat after hitting a usage limit, rather than at an end-of-day report * Per-branch commit checkpoints (`.contextzip/markers/eod.json`, `.contextzip/markers/handoff.json`) so every run only reports what's new since *that command* last ran on *that branch* — no manual bookkeeping, no date-based guessing * Three-case code-change resolution per file, checked in priority order: 1. **Not pushed** — diff against the upstream branch (covers committed- but-unpushed commits and uncommitted edits in one comparison) + the complete current file 2. **Diverged from Claude** — diff against the version Claude last produced, matched by filename against a local artifacts folder + the complete current codebase file 3. **Since last run** — diff against the stored marker commit, or the branch's merge-base with the default branch on a branch never tracked before + the complete current file Brand-new untracked files skip the diff (no baseline exists) and are included as full content only * Best-effort fetching of Claude's artifact files for case 2, using the user's own Claude.ai session key (`contextzip config --set-session-key`). Explicitly degrades gracefully — a missing key, an expired cookie, or the endpoint changing shape just skips case 2 with a warning, never fails the whole `eod`/`handoff` run * `contextzip config --set-session-key` / `--reset-session-key` — manage the Claude session key alongside the existing Gemini key; default `contextzip config` now shows both keys' status * Text-clipboard support (`clipboard.handle_text()`) — `eod`/`handoff` produce a prompt to paste, not a file to upload, so the existing file-clipboard tiers gained a text-mode sibling (`pbcopy` / `xclip`+`xsel` / `clip.exe`) * `exports/` added to the base exclusion ruleset (next to `.contextzip/`) — conversation exports and fetched Claude artifacts are contextzip's own working files, never code to package or report on ### Changed * `packager._ensure_gitignore()` generalised to accept an arbitrary entry instead of being hardcoded to `.contextzip/`, so `exports/` can register itself in `.gitignore` the same way on first use ### Developer Notes * New modules: `markers.py` (per-branch checkpoint storage), `code_changes.py` (the three-case decision tree), `claude_export.py` (locating the latest export, inline-vs-attach sizing), `claude_artifacts.py` (best-effort artifact fetching, rewritten on `httpx` for consistency with `ai/gemini.py` rather than introducing a second HTTP stack), `brief.py` (orchestrates `eod`/`handoff` end to end) * `git.py` gained branch-aware primitives: `get_current_branch()`, `get_default_branch()` (origin/HEAD detection with fallback chain), `get_merge_base()`, `is_ancestor()`, `get_upstream_branch()`, `diff_against()` (working-tree-aware — captures committed-since-baseline and uncommitted changes in one diff), `changed_files_against()` * A stored marker that's no longer an ancestor of `HEAD` (e.g. after a rebase) is detected via `is_ancestor()` and silently recomputed from the branch's merge-base, with a warning surfaced to the user rather than producing a nonsense diff * `claude_artifacts.py` matches Claude's flat artifact filenames against the codebase by basename only, since artifacts carry no knowledge of the repo's directory structure; ambiguous matches (same filename in multiple directories) pick the most recently modified candidate and surface a warning rather than guessing silently ## [0.3.5] — 2026-08-09 ### Added - **Monorepo-aware framework detection.** Detection now scans project_dir plus a shallow, bounded subdirectory tree (2 levels deep), instead of only checking the project root. A layout like `root/frontend/package.json` + `root/backend/requirements.txt` now correctly detects both Node.js and Python and stacks both exclusion rule sets — previously this fell through to only the universal base rules, leaving `node_modules/`, `.venv/`, and similar unexcluded except by whatever `.gitignore` happened to catch. The scan skips dependency/build directories (`node_modules/`, `.venv/`, `.git/`, etc.) so it never walks into a large dependency tree. Detection output now shows which subdirectory each ecosystem was found in, e.g. `Next.js (frontend/) + FastAPI (backend/)`. - **Configurable `.contextzip/` workspace location**, resolved in priority order: `CONTEXTZIP_WORKSPACE_LOCATION` env var → project config (`.contextzip.json`, committed, team-shared) → personal config (per-machine, via `contextzip config --set-workspace`) → default (`git-root`, unchanged behavior). Accepts `"git-root"`, `"cwd"`, or an explicit path. - `contextzip config --set-workspace ` / `--reset-workspace` — set or clear your personal workspace location default. - `contextzip config` (no flags) now shows the resolved workspace location and which config tier it came from. - New `.contextzip.json` project config file (optional, git-committed) for pinning `workspace_location` for every contributor. ### Fixed - `.gitignore` registration for the `.contextzip/` entry now correctly derives the git root independently, rather than assuming it's always the workspace directory's parent — a latent bug this release's configurable workspace location would otherwise have triggered (it only skips writing when the workspace is genuinely outside the repo, e.g. an absolute custom path). ### Removed - **`eod` and `handoff` commands**, along with the Claude.ai session-key config option they depended on. Both relied on an undocumented Claude.ai endpoint to fetch previously-produced artifacts, which was never a stable foundation for a shipped feature. - Removed modules: `brief.py`, `claude_export.py`, `claude_artifacts.py`, `code_changes.py`, `markers.py` - Removed CLI options: `contextzip config --set-session-key` / `--reset-session-key` - Removed config keys: `claude_session_key` - Also removed now-unused branch-diffing internals from `git.py` (`get_current_branch`, `get_upstream_branch`, `diff_against`, and related helpers) that existed solely to support `eod`/`handoff` - If you relied on `eod`/`handoff`, there's no replacement in this release — conversation exports and per-file diff prompts will need to be built by hand, or via your own script against `contextzip --git-changes`. ## [0.3.6] — 2026-08-23 ### Added * **`cz` CLI alias.** `cz` can now be used as a shorthand for `contextzip`, with identical commands and behavior. * **`.contextzip/` project workspace.** Contextzip now uses a dedicated workspace containing `config.json` and an `output/` directory for generated context archives. * **Project configuration via `.contextzip/config.json`.** Added persistent project-level preferences for `always_include`, `always_exclude`, and AI-assisted context selection. * **AI configuration.** Added configurable AI preferences including whether AI selection is enabled, the provider to use, and the maximum number of files AI can select. * **Configured file selection.** `always_include` and `always_exclude` preferences are now applied during context generation. * **Configurable AI file limits.** The configured `ai.max_files` value is now respected by the AI selection pipeline. * **Legacy `.contextzip.json` deprecation.** Project configuration is moving from `.contextzip.json` to `.contextzip/config.json`. ### Changed * Generated context archives are now organized under `.contextzip/output/`. * Contextzip configuration is now resolved through the new project configuration system while preserving the existing personal configuration for machine-specific settings and credentials. ## [0.3.7] — 2026-08-23 ### Added * **Visual config UI.** `contextzip config --ui` opens a local browser tab for setting `always_include`/`always_exclude` by clicking through your actual file tree, with live file counts and packed size as you go. * **Exclude suggestions.** The config UI automatically flags common non-code file types not already covered by the default exclusion rules — PDFs, office docs, fonts, media, design files, and anything over 1MB — as one-click "exclude all" chips. * **First-run offer.** A project with no config at all offers to launch the visual config UI on its first run. Declining is remembered — it won't ask again unless you run `contextzip config --ui` yourself. ### Changed * Declining the visual config UI's first-run offer is now stored in the personal config (`~/.config/contextzip/config.json`) so it's asked at most once per machine. ### Security * The local config UI binds to `127.0.0.1` only and requires a random per-session token on every request (matching Jupyter Notebook's approach) — nothing about the project's file structure leaves the machine. ## [0.3.8] — 2026-08-24 ### Added * **`apply-zip` command.** Closes the other half of the round trip: takes a ZIP handed back by an AI tool and writes it into the project. Every file is classified — new, modified, unchanged, drifted (changed or removed locally since it was sent), or untracked (no baseline to compare against) — before anything is written. * **Local sidecar manifest.** Every ZIP `contextzip` creates now also gets a `.manifest.json` written next to it in `.contextzip/output/` — a hash of each included file, taken at zip-time. It's written to disk only, **never inside the ZIP**, so it's never uploaded and never visible to whatever AI tool the ZIP is pasted into. * **`.contextzip/inbox/`.** Drop an AI-returned ZIP here and `apply-zip` finds it automatically; passing a path explicitly always overrides auto-detection. Multiple ZIPs in the inbox are listed and you're asked to be explicit rather than guessed at. * **Automatic backups.** Every file `apply-zip` is about to overwrite is copied first into `.contextzip/backups//`. * **`apply_zip()`** Python API function, mirroring `create_zip()`'s plain-function, no-Click, no-Rich style. * New `--dry-run`, `--verbose`, `--yes`, and `--manifest` flags on `apply-zip`. ### Changed * Consumed ZIPs are moved to `.contextzip/inbox/applied/-name.zip` rather than deleted, so they double as an audit trail alongside the backups. ### Security * `apply-zip` validates every ZIP entry's resolved path before extracting anything, refusing archives with path-traversal entries (`../`) that would write outside the project tree — no override flag, no exceptions. ### Notes * v1 of `apply-zip` only adds and modifies files — deletions are never inferred from a ZIP's contents. A file missing from a returned ZIP is left alone. ## [0.3.9] - 2026-08-26 ### Security - Config file permissions (`~/.config/contextzip/config.json`) are now locked to `0600` on **every** write, not just on creation. Previously a pre-existing config file from an older install (or one created under a permissive umask) could stay group/world-readable indefinitely — the file can hold a Gemini API key in plaintext, so this closes a real local exposure on shared machines. - The local config UI (`contextzip config --ui`) now compares the request token with `hmac.compare_digest` instead of `==`, removing a timing-based side channel on the single-use session token. - Substantially expanded the list of secret/credential files that are always excluded from packaged zips, regardless of framework: SSH private keys (`id_rsa`, `id_ed25519`, `id_ecdsa`, `id_dsa`), keystores (`.jks`, `.keystore`, `.p12`, `.pkcs12`, `.ppk`), CLI/package-manager credential files (`.npmrc`, `.netrc`, `.pypirc`, `.pgpass`, `.dockercfg`, Docker `config.json`), cloud provider credentials (`.aws/credentials`, `.aws/config`, `*serviceaccount*.json`, `kubeconfig`), and Terraform state (`*.tfstate*`, `.terraform/`), which routinely contains plaintext secrets even for "just infra" resources. ### Fixed - `apply-zip` now detects when every entry in an AI-returned zip is nested under one incidental top-level folder — the shape produced by `zip -r out.zip myfolder` or GitHub's "Download ZIP" — and strips it automatically when doing so clearly improves the match against the project's manifest, so files land at their real project-relative paths instead of recreating a wrapper folder. - `apply-zip` now warns before applying a zip whose structure barely matches the project at all (under 10% of paths found in the manifest, even after wrapper stripping), instead of silently treating every file as brand new. ### Changed - `.contextzip/inbox/applied/` now keeps only the most recently applied zip instead of accumulating one per session. ### Internal - Removed an unused `GitChanges` import in `api.py`. - Clarified the `.gitignore` comment describing what lives under `.contextzip/` and confirming the Gemini API key is never stored there. ## [0.4.0] - 2026-08-26 ### Added - **Config UI redesign.** `contextzip config --ui` is now a five-tab interface (Files, AI selection, Workspace, Advanced, Raw JSON) instead of a single always_include/always_exclude pane. The file tree and pattern list now render as a diff — a solid gutter and `+`/`-` marks on every row and pattern, matching the git-diff mental model the rest of the tool already uses. Still a single dependency-free HTML string with no build step, no Node.js, and no network calls from the page itself. - **New, persisted project preferences** (`.contextzip/config.json`), editable from the new UI tabs or by hand: - `ai.prompt_template` — house conventions prepended to every generated `prompt.txt`, ahead of the task description. - `limits.max_file_size_mb` — replaces the fixed 1 MB "large file" threshold with a per-project value. - `limits.redact_secrets` — reserved for a future scrub of secret-shaped values inside otherwise-included files. Persisted and editable now; not yet enforced during packaging (see Known limitations below). - `applied_zip_retention` — how many past `apply-zip` archives to keep in `.contextzip/inbox/applied/` before pruning (was hardcoded to 1). - `webui.auto_open` / `webui.port` — skip auto-launching a browser tab and/or pin the config UI to a fixed local port instead of a random free one. ### Changed - `contextzip config --ui`'s local server now exposes the full project config through `/api/state` and accepts the full config through `/api/save`, instead of only always_include/always_exclude. - Large-file warnings in normal CLI output now reflect the project's configured threshold instead of a fixed 1 MB. ### Known limitations - `limits.redact_secrets` can be toggled on and is saved correctly, but nothing reads it yet — no redaction currently happens during packaging. Flagging this explicitly so it isn't relied on before the enforcement lands. ## [0.4.1] — 2026-08-30 ### Added - **Per-mode output folders.** `.contextzip/output/` now splits by run mode instead of one flat folder: `output/codebase/` (plain `contextzip`), `output/git-changes/` (`--git-changes`), `output/prompt/` (`--prompt`, filename `vibe.zip`), and `output/watch/` (`debug-context.zip`). The folder name matches the flag that produced it, so it's unambiguous which zip is which without having to remember which command you last ran. - **Automatic workspace cleanup.** After every successful command (main run and `apply-zip`), `.contextzip/` is pruned automatically: old zip/manifest/report sets per mode folder, old `backups/` folders, and old archived zips in `inbox/applied/` are all deleted down to the most recent one — no confirmation, no separate command. New project settings: `cleanup.enabled` (default `true`) and `cleanup.keep_recent` (default `1`). - **`limits.redact_secrets` is now enforced.** Previously persisted in config and editable in the config UI but silently ignored by packaging (flagged as a known limitation in 0.4.0). Now, text files already going into the archive are scanned for secret-shaped values — AWS/Google/GitHub/Slack/Stripe/Anthropic/OpenAI keys, JWTs, private-key blocks, and a conservative generic `key`/`secret`/`token`/`password` assignment pattern — and matched values are replaced with `[REDACTED]` before writing. Only scans files already known to be text and within `limits.max_file_size_mb`; binary and oversized files are never touched. Redactions are surfaced in a terminal warning and itemized in the `.report.txt` file, never applied silently. - **`.report.txt` files.** Every packaging run (and `apply-zip` run) writes a full plain-text report alongside its ZIP — `codebase.report.txt` / `codebase.apply-report.txt` — containing everything that used to fill the terminal: the excluded-directory breakdown, the complete included-file list, per-file warnings, and (when applicable) which files had secrets redacted. ### Changed - **Terminal output redesigned.** Every command now prints a compact, one-line-per-step log (`✓` / `!` / `✗`) instead of boxed Rich panels and tables — matching the register of tools like `pnpm`, `vite`, and `ruff`. A full-width terminal is no longer needed to read a run. - The "Scanned" and "Packed" steps are now combined into a single line, e.g. `Scanned 1518 files & Packed 42 files · 42 included (394 KB), 1476 excluded, 394 KB → 126 KB, ↓68% smaller`. - Removed the `contextzip vX.X.X` / project-path banner from every command — output now starts directly at the first real step. - `apply-zip`'s manifest auto-detection now searches recursively across the new per-mode output subfolders, so it still finds the right manifest under the new layout. - Full file listings and excluded-directory breakdowns moved out of default terminal output — shown under `-v`/`--verbose`, or in the full report file. ### Removed - `.gitignore patterns applied` is no longer printed (no signal, dead weight). - Opening the output folder no longer prints a confirmation line. ## [0.4.2] — 2026-08-31 ### Added - **`limits.redact_secrets` is now enforced.** Previously persisted in config and editable in the config UI but silently ignored by packaging (flagged as a known limitation in 0.4.0). Now, text files already going into the archive are scanned for secret-shaped values — AWS/Google/GitHub/Slack/Stripe/Anthropic/OpenAI keys, JWTs, private-key blocks, and a conservative generic `key`/`secret`/`token`/`password` assignment pattern — and matched values are replaced with `[REDACTED]` before writing. Only scans files already known to be text and within `limits.max_file_size_mb`; binary and oversized files are never touched. Surfaced in a terminal warning and itemized in the `.report.txt` file, never applied silently. ### Changed - **`.contextzip/` is now fully local by default, no exceptions.** `config.json` was previously carved out of the workspace's `.gitignore` so it stayed trackable automatically. It's now ignored like everything else in the workspace — nothing under `.contextzip/` is ever pushed unless you explicitly opt in with `git add -f .contextzip/config.json`. ## [0.4.3] — 2026-09-20 ### Added - **`contextzip update` / `cz update`** — checks PyPI for a newer release and updates in place, so you no longer have to remember `pip install --upgrade contextzip` after every one of your own releases. Detects whether this install came from pip, pipx, or `uv tool` and re-invokes the matching one against the *currently running* interpreter — never a possibly-different `pip` off PATH. `--check` reports the current/latest version without installing; `--yes` skips the confirmation prompt. ### Fixed - **`apply-zip` no longer dumps a nested copy of the project when the returned zip wraps everything in a folder named after the project itself** (e.g. `project/`, or a folder matching the package's own name). The wrapper-stripping logic added in an earlier release had a guard that skipped stripping whenever a real folder with that name already existed in the project — which is exactly what happens when an AI tool names the wrapper after the project. Now, whenever a manifest is available, the decision is made by comparing how well paths actually match the manifest with vs. without stripping, not by guessing from what's on disk. - **`contextzip config --ui` now closes its browser tab automatically after a successful save** and hands control back to the terminal, instead of only telling you to close it yourself. ## [0.4.4] — 2026-09-21 ### Added - **`contextzip update` / `cz update`** — checks PyPI for a newer release and updates in place, so you no longer have to remember `pip install --upgrade contextzip` after every one of your own releases. Detects whether this install came from pip, pipx, or `uv tool` and re-invokes the matching one against the *currently running* interpreter — never a possibly-different `pip` off PATH. `--check` reports the current/latest version without installing; `--yes` skips the confirmation prompt. - **`.contextzip/inbox/README.txt`** is now created automatically the moment the `.contextzip/` workspace is first set up (and self-heals if it's ever missing) — a plain-language explainer of what the inbox is for, how to drop a zip into it, and what happens afterward, so the folder isn't a silent, unexplained thing you have to already know about. ### Fixed - **`apply-zip` no longer dumps a nested copy of the project** when the returned zip wraps everything in a folder named after the project itself (e.g. `project/`, or a folder matching the package's own name). The wrapper-stripping logic had a guard that skipped stripping whenever a real folder with that name already existed in the project — exactly what happens when an AI tool names the wrapper after the project. The decision is now made by comparing manifest match-rate with vs. without stripping instead of guessing from what's on disk. - **`apply-zip` no longer leaves a zip's wrapper folder in place when the freshest manifest is scoped to a small subset of the project** (most commonly `.contextzip/output/git-changes/changes.manifest.json`, which only ever tracked files touched in one prior git-diff run). A zip of otherwise-legitimate new files matched that manifest at 0% whether or not the wrapper was stripped, so the fix above's manifest-comparison saw a tie and played it safe by leaving the wrapper in place. It now falls back to comparing the zip's top-level folders against the project's actual current directory layout (`app/`, `components/`, `lib/`, etc.) whenever the manifest signal ties, and strips confidently when that layout lines up. - **`apply-zip ` no longer drops its `.apply-report.txt` next to wherever the zip happens to be** (e.g. the project root) when given an explicit path outside `.contextzip/inbox/`. The report now always writes to `.contextzip/inbox/`, matching where it's always shown up when a zip is auto-detected from there. The zip itself is still left in place, unmoved, when given an explicit path. - **Auto-cleanup now actually removes old `*.apply-report.txt` files sitting loose in `.contextzip/inbox/`** instead of leaving every one ever written to pile up indefinitely. Keeps only the most recent `cleanup.keep_recent` (default 1), same rule as everything else it prunes. ### Removed - **The per-apply backup mechanism (`.contextzip/backups//`) is gone entirely.** `apply-zip` no longer copies overwritten files there before writing; the manifest-diff confirmation prompt and the fact that the applied zip is always archived (not deleted) remain the safety net. Auto-cleanup now also wipes any leftover `.contextzip/backups/` folder outright on projects upgrading from an older contextzip version, rather than just pruning it down over time. ## [0.4.5] — 2026-09-27 ### Fixed - **`--git-changes` was silently ignoring `.contextzip/config.json`.** `_run()` only built the exclude/force-include rules from `always_include`/`always_exclude` — and folded in the current run's `--include`/`--exclude` — inside the standard-scan branch. `resolve_files_from_git()` had no parameters to receive any of that, so `--git-changes` packaged exactly what `git status` reported, no more and no less, regardless of what the config or the flags for that run said. A project could have `*.snap` in `always_exclude` and still see it land in every `--git-changes` zip. `resolve_files_from_git()` now accepts `extra_exclude`, `include_only`, and `force_include` and applies them with the same precedence `resolve_files()` already used — force-include first, then the safety floor plus excludes, then include-only — so both modes resolve a project's files identically. - **`contextzip watch` could package a file straight out of `always_exclude`** if a stack trace happened to name it — the debug-zip path built its file list from `extract_paths()` alone, with no awareness that a project had already asked for that file to be left out everywhere else. `process_buffer()` now filters extracted paths through the project's `always_exclude`/`always_include` before they reach `prompt.txt` or `source-files.zip`. - **The Python API (`get_files()`, `get_git_changes()`) never read `.contextzip/config.json` at all** — library callers got only the built-in rules plus whatever `include`/`exclude` they passed in explicitly, even on a project with a committed config the CLI already honored. Both functions now load project config by default and apply `always_include`/`always_exclude`/`limits.max_file_size_mb` the same way the CLI does. This is a behavior change for existing callers relying on the old, config-blind result — pass the new `use_project_config=False` to keep it. ### Changed - **Empty-result messages now read "Nothing to compress in this one"** instead of "Nothing to package" — shown both when `--git-changes` finds a clean working tree and when a standard scan excludes every file. - **Unknown-ecosystem detection now points you to request your language.** When contextzip can't identify a project's stack, the detection line is followed by `Don't see your language? Add it here: ` instead of leaving that one line as the only output. ## [0.4.6] — 2026-10-07 ### Fixed - **The ZIP was never actually copied to the clipboard on Windows or Linux.** On Windows there was no clipboard step at all: contextzip opened Explorer and left you to copy the file yourself. On Linux it wrote the ZIP's raw bytes to the clipboard as `application/zip`, which browsers ignore on paste, and only when `xclip` happened to be installed. Both platforms now put a real *file reference* on the clipboard, the same thing Ctrl+C on a file does, so you can paste the ZIP straight into Claude, ChatGPT or any other upload box. - **Windows:** sets `CF_HDROP` directly through the Win32 API (plus "Preferred DropEffect" so Explorer pastes a copy), then reads it back to confirm. If that fails it falls back to PowerShell `Set-Clipboard -LiteralPath`, and only then to opening Explorer. - **Linux:** sets `text/uri-list`, the target browsers read when you paste a file. Order of attempts: `wl-copy` (only if already installed, on Wayland), a new built-in X11 clipboard owner, then `xclip` (only if already installed). Every attempt is read back before contextzip reports success. - **A failed clipboard copy was silent.** When contextzip fell back to opening the folder it printed nothing, so you couldn't tell the copy had failed. It now says so and, when it knows, how to fix it. - **Explorer fallback lost the selection on paths with spaces or commas.** `explorer /select,` is now passed as one correctly quoted command line. - `xclip` / `wl-copy` are run with their output discarded rather than captured, so they can no longer block the CLI. ### Added - **Built-in X11 clipboard owner — nothing to install on Linux.** `pip install contextzip` is now enough: contextzip speaks the small part of the X11 protocol needed to own the clipboard itself (standard library only, no new dependency). It offers `text/uri-list`, `x-special/gnome-copied-files` and `application/x-kde-cutselection`, so browsers and GNOME/KDE file managers can all paste the file. A tiny background process holds the clipboard and exits on its own as soon as something else is copied. ### Changed - Headless and SSH Linux sessions (no display) now print the ZIP's path instead of trying to launch `xdg-open`. - `ClipboardResult` has a new optional `hint` field explaining why a copy fell back and how to fix it. - macOS: quotes and backslashes in the ZIP's path are now escaped in the Finder script. Behaviour is otherwise unchanged. ### Known limitations - Pasting on Wayland-native browsers relies on XWayland passing the clipboard through, which GNOME and KDE do. A Wayland-only setup without XWayland needs `wl-clipboard` (`wl-copy`) installed; otherwise contextzip opens the folder and tells you what to install. --- ## [0.5.0] — 2026-10-19 ### Added — versioned snapshots - **Every package is a numbered snapshot.** A copy of each ZIP and its manifest is kept in `.contextzip/snapshots/`, so `apply-zip` can merge a reply against exactly what was sent — even after you've packaged again, which used to delete the original. Kept while among the newest `snapshots.keep` (10), younger than `snapshots.max_age_days` (14), or awaited by a reply in the inbox. `-o` ZIPs are snapshotted too. - **`CONTEXTZIP.md` in every ZIP**: the snapshot ID plus short reply rules for the AI (complete files, same paths, leave `[REDACTED]` alone, list moves and deletions). A reply that echoes it is matched to its exact snapshot. Turn off with `"zip_guide": false` or in the config UI. - **`contextzip snapshots`** lists snapshots; **`apply-zip --snapshot ID`** pins one. - **Moves and deletions.** New statuses *moved* (declared in the reply's `CONTEXTZIP.md`, or near-identical content at a new path) and *delete-requested*. The old file is only removed with an explicit yes, `--remove-moved` / `--allow-delete`, or the review page's checkbox — never by `--yes` alone and never if you've changed it since. - **Non-git monorepos**: when a folder holds a few sibling repos and no git of its own (`shop/frontend/.git`, `shop/backend/.git`), contextzip asks once whether to use it as the project root and remembers the answer. - A notice when you run contextzip inside a git submodule. - Monorepo packages declared in npm/yarn/pnpm workspaces, `lerna.json`, Cargo workspaces or `go.work` are detected at any depth. - `.gitattributes` normalising line endings to LF. ### Fixed - When Gemini is rate-limited, the summary now says the files were picked by keyword match instead of crediting Gemini. - The Python API's `create_zip()` now honours the project's `limits.redact_secrets` setting, like the CLI. - Moved off pathspec's deprecated `gitwildmatch` factory to `GitIgnoreSpec` (same matching, pinned by tests). - **Monorepos: everything now anchors on the project root, from any folder.** Running `cz` inside `frontend/` used to create `frontend/.contextzip/`, write manifests relative to `frontend/`, and skip changes in sibling folders. Now every command — packaging, `--git-changes`, `apply-zip`, `watch` and the config UI — finds the project root first and works from there. - The root is the **outermost** git repository (not above your home folder), so a nested `frontend/.git` left by create-next-app or create-vite no longer hijacks it. A `.git` *file* (worktrees, submodules) now counts as a repo. Without git, an existing `.contextzip/` workspace marks the root. `CONTEXTZIP_ROOT` overrides. - Paths you type are relative to where you are, like `git add`: `cz include src/` from `frontend/` means `frontend/src/`. - `contextzip config` shows which root was picked and why. - An old `.contextzip/` left in a subfolder by earlier versions is pointed out as safe to delete. - **`--git-changes` missed changes inside nested repos.** An embedded repo (`frontend/.git`) or submodule appeared as one opaque entry and its changes were dropped. Nested repos are now read too. - **`--git-changes` mangled unusual file names.** Names with spaces or non-ASCII characters came back quoted and escaped; git's NUL-separated output is used now. - **`watch` from a subfolder** runs your command where you ran it and resolves stack-trace paths against that folder first. Bare file names are matched without walking `node_modules/`. - **`apply-zip` could write `[REDACTED]` over real secrets** if the AI returned a redacted file. Redacted files are now always merged against the redacted copy that was sent, keeping your real values. ### Changed - **`apply-zip` rebuilt.** - Files are matched to the project by path suffix, so ZIPs wrapped in extra folders, relative to a subfolder, flattened, or using backslash separators land in the right place; new files follow their siblings. Names that fit several files are settled by content, or marked *ambiguous* and never guessed. - The snapshot (manifest) that best matches the returned ZIP is used, not just the newest — no more everything-is-drifted after an unrelated run or a ZIP written with `-o` (those manifests are now registered in `.contextzip/output/external/`). - Three-way merge: when you and the AI changed a file in different places, the changes are merged (status *merged*). Only changes to the same lines are a *conflict*. - A file that already matches what the AI returned is *unchanged*, not drifted. Line endings never count as a change, and text is written with the local file's line endings and BOM. - New statuses: *merged*, *conflict*, *deleted*, *partial* (the AI returned `// ... rest of the file unchanged` instead of code) and *ambiguous*. - When something needs a decision, you choose: apply the safe changes, review in the browser, resolve in your editor, apply everything, or quit. Undecided files are left alone and the ZIP stays in the inbox. - Non-interactive runs (CI) write nothing when a decision is needed, unless `--safe-only` or `--yes` is given. - `__MACOSX/`, `.DS_Store` and echoed `prompt.txt` are ignored. - The Python API's `apply_zip()` now leaves files that need a decision alone unless `include_risky=True`. `ApplyResult` gains `skipped` and `zip_kept`. - API functions called without a `path` now use the project root, like the CLI. ### Added - `--here`: package only the folder you're in. - `apply-zip --review`: a local, token-protected review page with side-by-side diffs, word-level highlights and in-page conflict editing. - `apply-zip --editor`: resolve conflicts in VS Code's three-way merge editor (also Cursor, Windsurf, VSCodium; or set `CONTEXTZIP_EDITOR`). - `apply-zip --safe-only`. - `CONTEXTZIP_ROOT` to pin the project root. - Manifest format v2: records the ZIP's location, the project root, the redacted files and a line-ending-insensitive hash per text file. --- ## [0.4.7] — 2026-10-19 ### Changed - **A `.env` you deliberately include now has every value redacted.** Secret files (`.env`, private keys, credential files, Terraform state and so on) are still left out by default. You can still bring one back with `contextzip include .env`, `always_include` in `.contextzip/config.json` or `contextzip config --ui`, so the AI can see which variables your app expects. It is now never zipped as-is: - `.env` / `.env.*` files keep every key, comment and blank line, but every value becomes `[REDACTED]`. Multi-line quoted values collapse to one redacted line, and any line that isn't `KEY=value` is redacted whole. - Any other secret file gets the secret-pattern scan (private-key blocks, API keys, tokens and so on). - A secret file that isn't text (`.p12`, `.jks` and so on) can't be redacted, so it is left out and listed in the report. - This happens whatever `limits.redact_secrets` says and whatever the file's size. It applies to normal runs, `--git-changes`, `--prompt`, the `source-files.zip` inside `contextzip watch`'s debug package, and the Python API. - **`contextzip include` / `--include` now brings back paths you name explicitly**, even when the rules exclude them. `contextzip include .env`, `include dist/` and `include some-gitignored-file.txt` all work now. Previously they quietly matched nothing. Excluded folders nested inside a named folder are still skipped (`include dist/` leaves out `dist/node_modules/`). Naming an ordinary folder still only narrows the run, so `include src/` doesn't pull in `src/.env`. The CLI prints a line when this happens, and secret files are redacted as described above. This also works with `--git-changes` and the Python API's `get_files()`. - Include paths now accept a leading `./` or `.\` (`contextzip include ./src`). - The scan report now groups repeated redactions (`.env value x3`). ### Fixed - The "Don't see your language?" hint shown for undetected projects linked to a placeholder (`https://example.com`). It now points to . - **The report didn't list files that couldn't be written to the ZIP**, even though the CLI said "see report". They now appear under "Left out of the ZIP" with the reason. ### Developer notes - `rules/base.py` has a new `SECRET_PATTERNS` list. `PATTERNS` still contains it, so the default exclusions are unchanged. - New in `contextzip.redact`: `is_sensitive_path()`, `is_env_file()`, `redact_env()` and `redact_sensitive_file()`.