From 0c9e172ce178764260cf1f43a614e2fd9ca67a42 Mon Sep 17 00:00:00 2001 From: Vojtech <119944107+cvrysanek@users.noreply.github.com> Date: Tue, 19 May 2026 20:45:45 +0400 Subject: [PATCH] fix(init): chmod +x workspace hooks in OVERRIDE mode too (#359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `agnes init` ran `_chmod_workspace_hooks(workspace)` only in DEFAULT mode (inside `if not override_active:`). The OVERRIDE path — used for Initial-Workspace-Template seed-repo flows where the admin's template repo is cloned into the workspace — silently skipped chmod. When the seed repo's git checkout didn't preserve the +x bit (filemode=false, archive extractions, FUSE/NFS mounts), hooks like `.claude/hooks/skill-nudge/nudge.sh` and `.claude/hooks/prompt-history/log-prompt.sh` landed non-executable and every SessionStart fired `Permission denied`. The chmod helper itself already recurses (`rglob`) so the subdir-scoped hook layouts were covered — the bug was the call site. Moved the call out of the override gate to a common step before the first pull. Both DEFAULT and OVERRIDE paths now chmod hooks before the first SessionStart can fire. --- CHANGELOG.md | 15 +++++++++++++++ cli/commands/init.py | 17 ++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae3fd5a..5d38c3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,21 @@ CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every C ## [Unreleased] +### Fixed +- `agnes init` now runs `_chmod_workspace_hooks(workspace)` for OVERRIDE + mode too (Initial Workspace Template seed-repo flow), not just the + DEFAULT path. Override-mode workspaces seeded from an admin's + template repo were leaving hooks like + `.claude/hooks/skill-nudge/nudge.sh` and + `.claude/hooks/prompt-history/log-prompt.sh` non-executable when + the seed repo's git checkout didn't preserve the +x bit + (`core.filemode=false`, archive extractions, FUSE/NFS mounts), and + every SessionStart fired `Permission denied`. The chmod helper + already recurses (`rglob`) so subdir-scoped hook layouts were + covered — the bug was that the call site sat inside the + `if not override_active:` block. Moved out to a common step + before the first pull so every init path runs it. + ## [0.55.4] — 2026-05-19 ### Security diff --git a/cli/commands/init.py b/cli/commands/init.py index b35d555..dffe817 100644 --- a/cli/commands/init.py +++ b/cli/commands/init.py @@ -493,7 +493,6 @@ def init( ), encoding="utf-8") install_claude_hooks(workspace) install_claude_commands(workspace) - _chmod_workspace_hooks(workspace) # ------------------------------------------------------------------ # Step 6: CLAUDE.local.md stub — only when absent. `--force` does NOT @@ -511,6 +510,22 @@ def init( encoding="utf-8", ) + # ------------------------------------------------------------------ + # Always chmod +x hook scripts that landed on disk, regardless of + # which path seeded the workspace. In DEFAULT mode the hooks come + # from `install_claude_hooks` above; in OVERRIDE mode they come + # from the admin's initial-workspace-template clone — and `git + # checkout` of that template doesn't reliably preserve the +x bit + # (filemode=false repos, archive extractions, FUSE/NFS mounts), + # so hooks like `.claude/hooks/skill-nudge/nudge.sh` or + # `.claude/hooks/prompt-history/log-prompt.sh` could land non- + # executable and fire `Permission denied` on the very next + # SessionStart. `_chmod_workspace_hooks` recurses (`rglob`) so + # subdir-scoped hook layouts are covered. Best-effort, no-op on + # Windows NTFS. + # ------------------------------------------------------------------ + _chmod_workspace_hooks(workspace) + # ------------------------------------------------------------------ # Step 7: first pull. `run_pull` records per-stage failures inside # `result.errors` rather than raising for transient issues, so any