mirror of
https://github.com/daymade/claude-code-skills.git
synced 2026-09-14 16:15:21 +08:00
fix(repo): activate versioned mainline hook dispatchers (#395)
Wire the repository guard into real commit/push dispatchers, preserve the shared PII guard with exact stdin replay, cover extensionless hooks in CI, and pin linked worktrees to the canonical primary hook path.
This commit is contained in:
+17
-2
@@ -1,9 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Pre-commit hook: scan staged changes for sensitive data
|
||||
# Install: git config core.hooksPath .githooks
|
||||
# Repository dispatcher: block direct/stale mainline work, then preserve the
|
||||
# maintainer's shared PII guard when installed. Contributors without that shared
|
||||
# guard still receive the local fallback scan below.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
node "$REPO_ROOT/scripts/git-mainline-guard.mjs" pre-commit
|
||||
|
||||
GLOBAL_GUARD_DIR="${GIT_PII_GUARD_DIR:-${HOME:-}/scripts/git-pii-guard}"
|
||||
if [ -x "$GLOBAL_GUARD_DIR/pre-commit" ]; then
|
||||
GLOBAL_HOOK_DIR="$(cd "$(dirname "$GLOBAL_GUARD_DIR/pre-commit")" && pwd)"
|
||||
if [ "$GLOBAL_HOOK_DIR" != "$HOOK_DIR" ]; then
|
||||
exec "$GLOBAL_GUARD_DIR/pre-commit"
|
||||
fi
|
||||
fi
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# Repository dispatcher: replay the exact pre-push update set to both the
|
||||
# mainline/version guard and the maintainer's shared PII guard.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
HOOK_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
HOOK_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/claude-code-skills-pre-push.XXXXXX")"
|
||||
trap 'rm -rf -- "$HOOK_TMP_DIR"' EXIT
|
||||
UPDATE_INPUT="$HOOK_TMP_DIR/updates"
|
||||
cat >"$UPDATE_INPUT"
|
||||
|
||||
node "$REPO_ROOT/scripts/git-mainline-guard.mjs" pre-push "$@" <"$UPDATE_INPUT"
|
||||
|
||||
GLOBAL_GUARD_DIR="${GIT_PII_GUARD_DIR:-${HOME:-}/scripts/git-pii-guard}"
|
||||
if [ -x "$GLOBAL_GUARD_DIR/pre-push" ]; then
|
||||
GLOBAL_HOOK_DIR="$(cd "$(dirname "$GLOBAL_GUARD_DIR/pre-push")" && pwd)"
|
||||
if [ "$GLOBAL_HOOK_DIR" != "$HOOK_DIR" ]; then
|
||||
"$GLOBAL_GUARD_DIR/pre-push" "$@" <"$UPDATE_INPUT"
|
||||
fi
|
||||
else
|
||||
echo "WARNING: shared PII pre-push guard is unavailable; only the repository mainline guard ran." >&2
|
||||
fi
|
||||
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Local history skills** (`daymade-claude-code` v2.0.0; marketplace v2.0.0): replace three overlapping/ambiguous entry points with four provider-and-action-specific Skills: `claude-code-history-files-finder` → `read-claude-code-history`, `local-conversation-history` → `read-codex-history`, `continue-claude-work` → `continue-claude-code-work`, while `continue-codex-work` keeps its name. The two readers now own all parsing and remain evidence-only; the two continuation Skills consume verified read receipts, rebuild the original business outcome / unfulfilled requests / user corrections / proven prior assets, and only then execute a next action that directly reduces the outstanding result. `read-codex-history` keeps prompt ledger, state DB and rollout JSONL as separate evidence surfaces, adds exact raw-input tables, Codex-only search, verified selected identity and exact fork/compaction lineage, and rejects fused rollouts containing multiple `session_meta` IDs. `read-claude-code-history` adds recent inventory, chronological Session evidence with queued human prompts in place, original-word export grouped by Session, full-event search, hybrid recall, triage and deleted-file recovery. Exact readers now parse every physical record rather than a resume-oriented tail, retain every selected human and assistant text turn so a middle successful asset cannot disappear, cover registered Claude archives, require one matching record-level identity instead of trusting a filename, reject fused identities and divergent physical copies, choose a strict append-only live/archive superset without traversal-order last-wins, skip healthy blank separators, and fail visibly on malformed JSONL. The parser implementation and format references moved from the continuation bundles into their owning read bundles; all old runtime instructions remain directly reachable in migration references, including the legacy Kimi branch, rather than being silently deleted. Existing users should run `claude plugin marketplace update daymade-skills`, update/reinstall `daymade-claude-code@daymade-skills`, and replace old slash invocations with the mapping above. Verification: strict validation for all four Skills; Claude-reader and Codex-reader regression counts are recorded by the current test run; a real Claude active/archive replay retains the pre-compaction chronology, one real Codex continuation replay recovers the corrected second-attachment referent, and a separate misbound Session replay fails explicitly as a fused two-identity rollout instead of attributing another Session's events to it.
|
||||
|
||||
### Fixed
|
||||
- **Repository mainline guard:** make the local enforcement path real instead of documentary. The versioned pre-commit/pre-push dispatchers now invoke the mainline/version guard, replay the exact pre-push update set to the existing shared PII guard, and retain a local pre-commit PII fallback for contributors without the maintainer setup. Deterministic fixtures prove direct commits and pushes to `main` are blocked and that the shared pre-push scanner receives byte-identical input. The shared worktree config must point at the canonical primary checkout's **absolute** `.githooks` path; a relative path would let each stale worktree select its own stale dispatcher.
|
||||
- **claude-switch-models-setup** (`daymade-claude-code` v3.6.1; marketplace v3.4.1): finish the documentation side of the managed-source expansion. `CLAUDE.md` now names the implementation and `--print-watch-paths` as the authority instead of inviting another copied inventory; the architecture reference removes hand-maintained marketplace, checkout, and watcher-manifest lists; and the Skill, troubleshooting guide, and both READMEs stop persisting script/alias counts that are already derivable from their explicit lists. No runtime behavior changes.
|
||||
- **git-safety-net** (v1.12.0): make independent-clone retirement preserve more than a clean working tree. Checkout discovery now exposes `objects/info/alternates` borrowing instead of presenting a `git clone --shared` checkout as ordinary owned storage. A new non-destructive `git_prepare_clone_retirement.sh` refuses tracked/untracked/ignored bytes, stashes, shallow history, reflog-only commits, every clone-only unreachable Git object, partial/promisor clones, attached linked worktrees, local submodule repositories, known clone-private Git LFS/annex object stores, tracked content filters, unresolved repository-local config includes/custom hook paths, and mismatched survivor identity before creating a backup. It disables lazy fetch, repository fsmonitor, optional index refresh, and untracked-cache writes; freezes every ref tip plus symbolic-ref topology (including stale remote-tracking refs and `HEAD`); builds a no-prerequisite all-refs bundle; preserves reflog identities plus config/hooks/info bytes/types/modes; and binds bundle and metadata archives to SHA-256 receipts. `--verify-current` rechecks refs/symrefs, reflog, metadata, physical state, linked-worktree/submodule/promisor/extension-store inventory, unreachable objects, and artifact digests. The retirement contract defaults to an explicitly authorized recoverable quarantine/OS Trash move, keeps permanent deletion separate, and freezes the absent destination plus process occupancy before the final verification so the no-clobber quarantine can be the next operation instead of leaving an avoidable post-verification probe. Thirty-six deterministic regressions cover shared-object discovery, empty-repository restore plus symbolic-ref replay, dirty/ignored/stash gates, shallow and unreachable history/object types, promisor non-mutation, extension-store/filter refusal, fsmonitor non-execution, linked-worktree/submodule refusal, config/hook indirection, hook-mode/ref/metadata races, and archive tampering.
|
||||
- **skill-creator** (`daymade-skill` v1.32.0): replace the high-frequency `uv run --with` overlays used by bundled tooling with one locked, project-local uv environment. `pyproject.toml` and `uv.lock` pin PyYAML, tiktoken, and pytest; all normal tool invocations now use `uv run --frozen`, so caller projects remain isolated while package data still comes from uv's shared global cache. The runtime contract explicitly rejects a cross-project `UV_PROJECT_ENVIRONMENT`, project-specific cache roots, and cache cleanup as part of ordinary Skill execution. The full local suite passes (`181 passed, 7 subtests`) and the validator self-test passes 27/27.
|
||||
|
||||
@@ -205,9 +205,15 @@ Squash-merged PRs rewrite commits under new SHAs, so every direct commit to
|
||||
local `main` guarantees divergence the moment its PR merges. Two rules keep
|
||||
`main` clean:
|
||||
|
||||
`scripts/git-mainline-guard.mjs` enforces this at commit/push time and also
|
||||
rejects stale marketplace manifests or reused plugin versions against current
|
||||
`origin/main`; CI runs the same version-progression check on every PR.
|
||||
`.githooks/pre-commit` and `.githooks/pre-push` dispatch to
|
||||
`scripts/git-mainline-guard.mjs`, which rejects direct local-main work and stale
|
||||
marketplace manifests or reused plugin versions against current main. The
|
||||
dispatchers preserve the shared PII guard when it is installed. Activate this
|
||||
repository **from the canonical primary main checkout** with
|
||||
`git config core.hooksPath "$(pwd -P)/.githooks"`. The absolute path matters:
|
||||
`core.hooksPath` is shared by linked worktrees, so a relative path would let a
|
||||
stale feature worktree select its own stale dispatcher. CI and the GitHub main
|
||||
ruleset independently require the same release checks on every PR.
|
||||
|
||||
1. **Never commit directly to local `main`.** All work starts on a feature
|
||||
branch (`git checkout -b <topic>`), ships via PR, and lands by squash merge.
|
||||
@@ -268,7 +274,7 @@ Skills for public distribution must NOT contain:
|
||||
4. **gitleaks** (`.gitleaks.toml`) — deep scan with custom rules for this repo
|
||||
5. **AI semantic read-through** (the gate the other four structurally cannot be) — layers 1-4 are keyword/regex/gitleaks: they only match patterns someone listed, and are blind to private content with **no keyword** — a real name in another language (gitleaks doesn't cover CJK), a verbatim line from a real transcript, a real example dropped into an illustration. Before publishing, **read the whole skill yourself and judge each concrete name/example/snippet semantically** ("generic placeholder / public entity, or lifted from a real project / person / transcript?"). A green scan is **not** a clean bill of health; "grep found nothing" only means your word list didn't fire. Method: [`daymade-skill/skill-creator/references/sanitization_checklist.md`](./daymade-skill/skill-creator/references/sanitization_checklist.md).
|
||||
|
||||
PII Guard is enabled via `~/scripts/git-pii-guard/manage.sh enable <repo-path>`, which sets `core.hooksPath` to `~/scripts/git-pii-guard`.
|
||||
Most repositories enable PII Guard via `~/scripts/git-pii-guard/manage.sh enable <repo-path>`. This repository instead points `core.hooksPath` at the canonical primary checkout's absolute `.githooks` directory: its versioned dispatchers run the repository mainline guard and then delegate to the same shared PII guard when installed.
|
||||
For repo-specific additions:
|
||||
- `.pii-patterns` — extra content regexes
|
||||
- `.pii-path-patterns` — extra forbidden path regexes
|
||||
|
||||
@@ -26,12 +26,14 @@ while IFS= read -r script; do
|
||||
printf '%s\n' "$output" | sed 's/^/ /'
|
||||
fi
|
||||
done < <(
|
||||
find . -name '*.sh' -type f \
|
||||
-not -path './.git/*' \
|
||||
-not -path '*/node_modules/*' \
|
||||
-not -path '*/.venv/*' \
|
||||
-not -path '*/vendor/*' \
|
||||
| sort
|
||||
{
|
||||
find . -name '*.sh' -type f \
|
||||
-not -path './.git/*' \
|
||||
-not -path '*/node_modules/*' \
|
||||
-not -path '*/.venv/*' \
|
||||
-not -path '*/vendor/*'
|
||||
find .githooks -maxdepth 1 -type f 2>/dev/null
|
||||
} | sort -u
|
||||
)
|
||||
|
||||
if [ "$failed" -gt 0 ]; then
|
||||
|
||||
@@ -12,6 +12,8 @@ from pathlib import Path
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
GUARD_SOURCE = REPO_ROOT / "scripts/git-mainline-guard.mjs"
|
||||
CHECKER_SOURCE = REPO_ROOT / "scripts/ci/check_version_progression.py"
|
||||
PRE_COMMIT_SOURCE = REPO_ROOT / ".githooks/pre-commit"
|
||||
PRE_PUSH_SOURCE = REPO_ROOT / ".githooks/pre-push"
|
||||
|
||||
|
||||
def run(
|
||||
@@ -49,10 +51,13 @@ class MainlineGuardTests(unittest.TestCase):
|
||||
run(self.repo, "git", "config", "user.email", "test@example.invalid")
|
||||
run(self.repo, "git", "config", "user.name", "Test")
|
||||
(self.repo / "scripts/ci").mkdir(parents=True)
|
||||
(self.repo / ".githooks").mkdir()
|
||||
(self.repo / ".claude-plugin").mkdir()
|
||||
(self.repo / "daymade-audio/transcript-fixer").mkdir(parents=True)
|
||||
shutil.copy2(GUARD_SOURCE, self.repo / "scripts/git-mainline-guard.mjs")
|
||||
shutil.copy2(CHECKER_SOURCE, self.repo / "scripts/ci/check_version_progression.py")
|
||||
shutil.copy2(PRE_COMMIT_SOURCE, self.repo / ".githooks/pre-commit")
|
||||
shutil.copy2(PRE_PUSH_SOURCE, self.repo / ".githooks/pre-push")
|
||||
(self.repo / ".claude-plugin/marketplace.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
@@ -104,6 +109,22 @@ class MainlineGuardTests(unittest.TestCase):
|
||||
check=False,
|
||||
)
|
||||
|
||||
def hook(
|
||||
self,
|
||||
name: str,
|
||||
input_text: str | None = None,
|
||||
*extra: str,
|
||||
env: dict[str, str] | None = None,
|
||||
):
|
||||
return run(
|
||||
self.repo,
|
||||
str(self.repo / ".githooks" / name),
|
||||
*extra,
|
||||
input_text=input_text,
|
||||
env=env or self.env,
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_pre_commit_blocks_main(self) -> None:
|
||||
result = self.guard("pre-commit")
|
||||
self.assertEqual(result.returncode, 1)
|
||||
@@ -200,6 +221,61 @@ class MainlineGuardTests(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn("version did not strictly increase", result.stderr)
|
||||
|
||||
def test_versioned_pre_commit_dispatcher_blocks_local_main(self) -> None:
|
||||
env = self.env.copy()
|
||||
env["GIT_PII_GUARD_DIR"] = str(self.root / "missing-global-guard")
|
||||
result = self.hook("pre-commit", env=env)
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn("read-only runtime mirror", result.stderr)
|
||||
|
||||
def test_versioned_pre_push_dispatcher_blocks_direct_main(self) -> None:
|
||||
env = self.env.copy()
|
||||
env["GIT_PII_GUARD_DIR"] = str(self.root / "missing-global-guard")
|
||||
sha = run(self.repo, "git", "rev-parse", "HEAD").stdout.strip()
|
||||
line = f"refs/heads/main {sha} refs/heads/main {sha}\n"
|
||||
result = self.hook("pre-push", line, "origin", str(self.remote), env=env)
|
||||
self.assertEqual(result.returncode, 1)
|
||||
self.assertIn("direct pushes to main are forbidden", result.stderr)
|
||||
|
||||
def test_pre_push_dispatcher_replays_input_to_global_pii_guard(self) -> None:
|
||||
run(self.repo, "git", "switch", "-qc", "feature")
|
||||
(self.repo / "README.md").write_text("docs\n", encoding="utf-8")
|
||||
run(self.repo, "git", "add", "README.md")
|
||||
run(self.repo, "git", "commit", "-qm", "docs")
|
||||
sha = run(self.repo, "git", "rev-parse", "HEAD").stdout.strip()
|
||||
line = f"refs/heads/feature {sha} refs/heads/feature {'0' * 40}\n"
|
||||
|
||||
fake_guard = self.root / "fake-global-guard"
|
||||
fake_guard.mkdir()
|
||||
capture = self.root / "captured-push-input"
|
||||
fake_hook = fake_guard / "pre-push"
|
||||
fake_hook.write_text(
|
||||
"#!/bin/sh\nset -eu\ncat >\"$HOOK_CAPTURE_PATH\"\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
fake_hook.chmod(0o755)
|
||||
env = self.env.copy()
|
||||
env["GIT_PII_GUARD_DIR"] = str(fake_guard)
|
||||
env["HOOK_CAPTURE_PATH"] = str(capture)
|
||||
|
||||
result = self.hook("pre-push", line, "origin", str(self.remote), env=env)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(capture.read_text(encoding="utf-8"), line)
|
||||
|
||||
def test_pre_push_dispatcher_warns_when_shared_pii_guard_is_missing(self) -> None:
|
||||
run(self.repo, "git", "switch", "-qc", "feature")
|
||||
(self.repo / "README.md").write_text("docs\n", encoding="utf-8")
|
||||
run(self.repo, "git", "add", "README.md")
|
||||
run(self.repo, "git", "commit", "-qm", "docs")
|
||||
sha = run(self.repo, "git", "rev-parse", "HEAD").stdout.strip()
|
||||
line = f"refs/heads/feature {sha} refs/heads/feature {'0' * 40}\n"
|
||||
env = self.env.copy()
|
||||
env["GIT_PII_GUARD_DIR"] = str(self.root / "missing-global-guard")
|
||||
|
||||
result = self.hook("pre-push", line, "origin", str(self.remote), env=env)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("shared PII pre-push guard is unavailable", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user