diff --git a/CHANGELOG.md b/CHANGELOG.md index fa9e77e..5fc506b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [1.0.1] - 2026-06-19 + +### Changed + +- Updated the quickstart to install from the exact `v1.0.1` release tag and documented + that the pin must move with future releases. +- Changed force installs to replace the validated destination directory so stale files + cannot survive across versions. +- Added a GitHub hardening checklist for repository-side enforcement alongside Trellis. + ## [1.0.0] - 2026-06-19 First stable release of `codebase-trellis`. diff --git a/README.md b/README.md index 11a0bf2..36219a1 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,16 @@ It is not a Git command wrapper. The default behavior is inspection and planning ## Quickstart -Clone this repository locally first, then run the install script from the repository root. +Clone this repository locally, check out the exact release tag, then run the install +script from the repository root. **Claude Code - PowerShell** ```powershell git clone https://github.com/Shaelz/codebase-trellis-skill.git cd codebase-trellis-skill +git fetch --tags +git checkout v1.0.1 .\scripts\install-user.ps1 ``` @@ -38,11 +41,17 @@ cd codebase-trellis-skill ```bash git clone https://github.com/Shaelz/codebase-trellis-skill.git cd codebase-trellis-skill +git fetch --tags +git checkout v1.0.1 bash scripts/install-user.sh ``` Then restart Claude Code and type `/codebase-trellis` in any project. +These commands intentionally pin release `v1.0.1`. +The pinned tag must be updated here whenever a newer release is published. +Advanced users may install from `main` only when intentionally testing unreleased changes. + ## Install paths | Goal | Run from | Command | @@ -54,7 +63,10 @@ Then restart Claude Code and type `/codebase-trellis` in any project. User-level install makes the skill available in all projects. Project-local install adds it only to the current project's `.claude/skills/` directory. For project-local installs, navigate to your project root first, then call the script using the full path to where you cloned this repo. -If an installation already exists, the scripts exit with an error unless you pass `-Force` (PowerShell) or `--force` (bash). +If an installation already exists, the scripts exit with an error unless you pass +`-Force` (PowerShell) or `--force` (bash). Force mode removes the existing +`codebase-trellis` skill directory after validating its exact expected path, then copies +the selected version. This prevents stale files from surviving an upgrade. ## Usage @@ -90,6 +102,9 @@ Trellis operates within four safety layers: These layers are not equivalent. A skill rule does not substitute for a protected branch. A local grep does not substitute for GitHub secret scanning. +For a practical enforcement checklist, see +[GitHub hardening for Trellis](docs/github-hardening.md). + ## What Trellis never does without explicit approval - `git add .` diff --git a/docs/github-hardening.md b/docs/github-hardening.md new file mode 100644 index 0000000..1404862 --- /dev/null +++ b/docs/github-hardening.md @@ -0,0 +1,28 @@ +# GitHub hardening for Trellis + +Trellis provides behavioral guidance, inspection, and approval gates for AI-assisted +Git work. GitHub branch protections and rulesets provide repository-side enforcement. +Use both when changes to protected branches must be controlled regardless of which +developer, agent, or local tool performs the Git operation. + +Availability varies by repository visibility, GitHub plan, and organization settings. +Use the controls available to your repository and verify the resulting rules directly. + +## Practical checklist + +- Protect `main` with a branch protection rule or ruleset. +- Require pull requests before changes can merge into `main`. +- Require the existing `verify` workflow status check before merge. +- Block force pushes to protected branches. +- Block protected branch deletion where appropriate. +- Enable secret scanning and push protection where available. +- Keep GitHub Actions permissions least-privilege and read-only by default. Grant write + access only to workflows that require it. +- Protect release tags such as `v*` from deletion or retagging where available. + +Local Git hooks and Claude Code hooks can add useful checks earlier in the workflow. +They are optional companion layers, not substitutes for GitHub protections, because +local controls can be absent, bypassed, or configured differently on another machine. + +After configuring protections, test them with a pull request and confirm that GitHub +blocks merge until the required `verify` check passes. diff --git a/scripts/install-project.ps1 b/scripts/install-project.ps1 index df78031..b76dc59 100644 --- a/scripts/install-project.ps1 +++ b/scripts/install-project.ps1 @@ -6,8 +6,9 @@ Run this from the root of the project where you want to install the skill. .PARAMETER Force - Overwrite an existing installation. Without this flag the script exits if the - destination already exists. + Replace an existing installation. Without this flag the script exits if the + destination already exists. Replacement removes the validated skill directory + before copying so stale files cannot survive. .EXAMPLE .\path\to\install-project.ps1 @@ -23,7 +24,9 @@ $ErrorActionPreference = 'Stop' $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $sourceDir = Join-Path $scriptDir '..\skills\codebase-trellis' -$destDir = Join-Path (Get-Location) '.claude\skills\codebase-trellis' +$projectRoot = [System.IO.Path]::GetFullPath((Get-Location).Path) +$expectedParent = [System.IO.Path]::GetFullPath((Join-Path $projectRoot '.claude\skills')) +$destDir = [System.IO.Path]::GetFullPath((Join-Path $expectedParent 'codebase-trellis')) $sourceDir = (Resolve-Path $sourceDir).Path @@ -35,7 +38,17 @@ if (Test-Path $destDir) { Write-Error "Destination already exists: $destDir`nRe-run with -Force to overwrite." exit 1 } - Write-Host "[-Force] Overwriting existing installation." + $hasExpectedParent = [System.StringComparer]::OrdinalIgnoreCase.Equals( + [System.IO.Path]::GetDirectoryName($destDir), + $expectedParent + ) + $hasExpectedLeaf = [System.IO.Path]::GetFileName($destDir) -eq 'codebase-trellis' + if (-not $hasExpectedParent -or -not $hasExpectedLeaf) { + Write-Error "Refusing to remove unexpected destination: $destDir" + exit 1 + } + Write-Host "[-Force] Removing existing installation." + Remove-Item -LiteralPath $destDir -Recurse -Force } if (-not (Test-Path $destDir)) { diff --git a/scripts/install-project.sh b/scripts/install-project.sh index ddb4f5d..3b6f847 100644 --- a/scripts/install-project.sh +++ b/scripts/install-project.sh @@ -11,7 +11,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCE_DIR="$(cd "$SCRIPT_DIR/../skills/codebase-trellis" && pwd)" -DEST_DIR="$(pwd)/.claude/skills/codebase-trellis" +PROJECT_ROOT="$(pwd)" +EXPECTED_PARENT_DIR="$PROJECT_ROOT/.claude/skills" +DEST_DIR="$EXPECTED_PARENT_DIR/codebase-trellis" FORCE=false for arg in "$@"; do @@ -24,13 +26,18 @@ done echo "Source : $SOURCE_DIR" echo "Dest : $DEST_DIR" -if [ -d "$DEST_DIR" ]; then +if [ -e "$DEST_DIR" ] || [ -L "$DEST_DIR" ]; then if [ "$FORCE" = false ]; then echo "Error: destination already exists: $DEST_DIR" >&2 echo "Re-run with --force to overwrite." >&2 exit 1 fi - echo "[--force] Overwriting existing installation." + if [ "$(dirname "$DEST_DIR")" != "$EXPECTED_PARENT_DIR" ] || [ "$(basename "$DEST_DIR")" != "codebase-trellis" ]; then + echo "Error: refusing to remove unexpected destination: $DEST_DIR" >&2 + exit 1 + fi + echo "[--force] Removing existing installation." + rm -rf -- "$DEST_DIR" fi mkdir -p "$DEST_DIR" diff --git a/scripts/install-user.ps1 b/scripts/install-user.ps1 index 8463199..3bf9941 100644 --- a/scripts/install-user.ps1 +++ b/scripts/install-user.ps1 @@ -4,8 +4,9 @@ Installs the codebase-trellis skill to the user-level Claude Code skills directory. .PARAMETER Force - Overwrite an existing installation. Without this flag the script exits if the - destination already exists. + Replace an existing installation. Without this flag the script exits if the + destination already exists. Replacement removes the validated skill directory + before copying so stale files cannot survive. .EXAMPLE .\scripts\install-user.ps1 @@ -21,7 +22,8 @@ $ErrorActionPreference = 'Stop' $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $sourceDir = Join-Path $scriptDir '..\skills\codebase-trellis' -$destDir = Join-Path $HOME '.claude\skills\codebase-trellis' +$expectedParent = [System.IO.Path]::GetFullPath((Join-Path $HOME '.claude\skills')) +$destDir = [System.IO.Path]::GetFullPath((Join-Path $expectedParent 'codebase-trellis')) $sourceDir = (Resolve-Path $sourceDir).Path @@ -33,7 +35,17 @@ if (Test-Path $destDir) { Write-Error "Destination already exists: $destDir`nRe-run with -Force to overwrite." exit 1 } - Write-Host "[-Force] Overwriting existing installation." + $hasExpectedParent = [System.StringComparer]::OrdinalIgnoreCase.Equals( + [System.IO.Path]::GetDirectoryName($destDir), + $expectedParent + ) + $hasExpectedLeaf = [System.IO.Path]::GetFileName($destDir) -eq 'codebase-trellis' + if (-not $hasExpectedParent -or -not $hasExpectedLeaf) { + Write-Error "Refusing to remove unexpected destination: $destDir" + exit 1 + } + Write-Host "[-Force] Removing existing installation." + Remove-Item -LiteralPath $destDir -Recurse -Force } if (-not (Test-Path $destDir)) { diff --git a/scripts/install-user.sh b/scripts/install-user.sh index 770e4c0..1e940e8 100644 --- a/scripts/install-user.sh +++ b/scripts/install-user.sh @@ -9,7 +9,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCE_DIR="$(cd "$SCRIPT_DIR/../skills/codebase-trellis" && pwd)" -DEST_DIR="$HOME/.claude/skills/codebase-trellis" +EXPECTED_PARENT_DIR="$HOME/.claude/skills" +DEST_DIR="$EXPECTED_PARENT_DIR/codebase-trellis" FORCE=false for arg in "$@"; do @@ -22,13 +23,18 @@ done echo "Source : $SOURCE_DIR" echo "Dest : $DEST_DIR" -if [ -d "$DEST_DIR" ]; then +if [ -e "$DEST_DIR" ] || [ -L "$DEST_DIR" ]; then if [ "$FORCE" = false ]; then echo "Error: destination already exists: $DEST_DIR" >&2 echo "Re-run with --force to overwrite." >&2 exit 1 fi - echo "[--force] Overwriting existing installation." + if [ "$(dirname "$DEST_DIR")" != "$EXPECTED_PARENT_DIR" ] || [ "$(basename "$DEST_DIR")" != "codebase-trellis" ]; then + echo "Error: refusing to remove unexpected destination: $DEST_DIR" >&2 + exit 1 + fi + echo "[--force] Removing existing installation." + rm -rf -- "$DEST_DIR" fi mkdir -p "$DEST_DIR" diff --git a/scripts/verify-skill-package.ps1 b/scripts/verify-skill-package.ps1 index 8c5a810..5853e9a 100644 --- a/scripts/verify-skill-package.ps1 +++ b/scripts/verify-skill-package.ps1 @@ -44,6 +44,7 @@ Check-File "scripts\check-ascii-punctuation.ps1" Check-File "docs\FUTURE_BRANCHES.md" Check-File "docs\V1_RELEASE_PLAN.md" Check-File "docs\design-decisions.md" +Check-File "docs\github-hardening.md" Check-File "docs\source-review.md" Check-File ".gitignore" Check-File "README.md" diff --git a/scripts/verify-skill-package.sh b/scripts/verify-skill-package.sh index 050ea43..61cc38f 100644 --- a/scripts/verify-skill-package.sh +++ b/scripts/verify-skill-package.sh @@ -39,6 +39,7 @@ check_file "scripts/check-ascii-punctuation.ps1" check_file "docs/FUTURE_BRANCHES.md" check_file "docs/V1_RELEASE_PLAN.md" check_file "docs/design-decisions.md" +check_file "docs/github-hardening.md" check_file "docs/source-review.md" check_file ".gitignore" check_file "README.md"