IF THE PROJECT PATH IS /workspace/ — YOU ARE IN A CCY CONTAINER.
- NEVER run Ansible playbooks in the container
- Only edit and commit — then tell the user to deploy on their HOST system
- CCY version bump required when modifying
files/var/local/claude-yolo/claude-yolo
Full container rules and ctrl+z patch details: @CLAUDE/ContainerRules.md
This is the #1 principle of this project. It is non-negotiable.
- Exit immediately on errors — Use
set -ein all bash scripts - No silent failures — Every error must stop execution with clear message
- NEVER skip and continue — If an operation should succeed, FAIL on error
- NEVER decouple dependent operations — If task B depends on task A, failure in A must prevent B
- ❌
failed_when: false— PROHIBITED unless annotated with# FAIL-FAST-OK: <reason> - ❌
ignore_errors: true— PROHIBITED unless annotated with# FAIL-FAST-OK: <reason> - ❌ "Skip and warn" pattern — NEVER use
debugto warn and continue - ✅ Probe-then-fail pattern —
failed_when: falseis OK when registered result is explicitly checked
This is a public repository. Never commit personal information, credentials, hardcoded paths, or sensitive data. Always use Ansible variables, placeholders, and Vault encryption.
Full security rules, vault management, and pre-commit checks: @CLAUDE/SecurityRules.md
ALL system changes MUST go through Ansible playbooks. Never perform manual file copies, installations, service management, or configuration edits.
Full IaC workflow (edit → playbook → deploy → test): @CLAUDE/InfrastructureAsCode.md
Run ./scripts/qa-all.bash before every commit touching Bash or Python files. Run ESLint for extension JavaScript. Run ./scripts/qa-ctrl-z-patch.bash for CCY patch changes.
Full QA reference (scripts, what they check, limitations): @CLAUDE/QA.md
When providing diagnostic commands to users, always use --no-pager, | cat, or | head. Never open pagers or editors.
Full rules and examples: @CLAUDE/DebugCommands.md
Use Podman wherever possible — it is the better system. Reach for Docker only when a tool genuinely needs it for compatibility or legacy reasons, and understand that Docker is significantly less secure than Podman.
- Podman (rootless) — default for everything. CCY, devtools, ad-hoc work.
- Docker (rootful) — compatibility mode only, e.g. DDEV.
dockergroup = root-equivalent. - LXC (rootful) — VM-like full-system containers with systemd inside.
New playbooks needing a container engine must use the container_engine variable (default podman), not hardcode an engine.
Full role split, coexistence, and FAQ: @CLAUDE/ContainerEngines.md
- YAGNI — Don't add features until actually needed. No speculative code. Delete unused code.
- DRY — Extract common patterns. Use variables for repeated values. Reference, don't duplicate.
- Idempotent — All operations safe to run multiple times. Use
creates, conditionals, declarative state. - Security First — Never hardcode secrets. Validate inputs. Least privilege. No credentials in logs.
- Self-documenting — Clear names over comments. Comments explain WHY, not what.
Full Ansible style rules (playbook structure, markers, packages, services, variables, tasks): @CLAUDE/AnsibleStyle.md
Never let plan state lag behind the work it tracks. When code work completes, advances, or invalidates a plan task, the corresponding plan file changes must be committed too — ideally in the same commit as the code, but at minimum within the same session.
This rule exists to prevent drift between plan state and code state. It is not a restriction on when plans can be committed.
- Committing a brand-new plan on its own, with no related code yet — fine and recommended so the plan is tracked immediately
- Committing plan research, decision-gate notes, or status updates on their own
- Committing plan progress in the same commit as the code that implements it (preferred when both change in one session)
- Committing code that completes plan tasks while leaving the plan file unchanged on disk
- Leaving an untracked
CLAUDE/Plan/NNN-…/directory after committing related work - Marking tasks ✅ in conversation but not in the plan file
- Bundling unrelated plan edits with unrelated code changes — split them into separate commits
git status # Look for untracked CLAUDE/Plan/ dirs and unstaged plan edits
git add CLAUDE/Plan/NNN-description/ # Stage plan alongside related codeIf git status shows plan files modified by your session, decide before committing: stage them with the related code, or make a separate plan-only commit. Do not leave them dangling.
| File | Content |
|---|---|
| @CLAUDE/ContainerRules.md | CCY container detection, version bump, ctrl+z patch |
| @CLAUDE/ContainerEngines.md | Podman/Docker/LXC role split; when to use which; security trade-offs |
| @CLAUDE/InfrastructureAsCode.md | Ansible-only workflow, prohibited manual actions |
| @CLAUDE/AnsibleStyle.md | Playbook structure, markers, packages, services, variables |
| @CLAUDE/SecurityRules.md | Public repo warning, vault management, pre-commit checks |
| @CLAUDE/QA.md | QA scripts reference, what to run when |
| @CLAUDE/DebugCommands.md | Non-interactive command rules for user diagnostics |
| @CLAUDE/GnomeShell.md | GNOME Shell extension development (Wayland, ESLint, APIs) |
| @CLAUDE/PlanWorkflow.md | Planning workflow and plan document structure |
User-facing docs (installation, architecture, playbooks, troubleshooting) are in docs/. See docs/README.md for the full index.
This project uses claude-code-hooks-daemon for automated safety and workflow enforcement.
After editing .claude/hooks-daemon.yaml — restart the daemon using the hooks-daemon skill:
- Restart: use the
hooks-daemonskill with argsrestart - Health check: use the
hooks-daemonskill with argshealth
Important:
/hooks-daemonis a skill (slash command), not a bash command. Invoke it using the Skill tool, e.g.Skill(skill="hooks-daemon", args="restart"). Do NOT attempt to run/hooks-daemonas a bash command — it will fail.
Key files:
.claude/hooks-daemon.yaml— handler configuration (enable/disable handlers).claude/hooks/handlers/— project-specific custom handlers
Documentation: .claude/hooks-daemon/CLAUDE/LLM-INSTALL.md
The handlers listed below are active in this project. Read this section to avoid triggering unnecessary blocks.
When a tool is blocked by a handler, do not stop working. Read the block reason, modify your approach, and continue with your task.
The Read, Write, and Edit tools require absolute paths. Relative paths are blocked.
- Correct:
/workspace/src/main.py,/workspace/tests/test_utils.py - Blocked:
src/main.py,./config.yaml,../other/file.txt
The working directory is /workspace. Prepend /workspace/ to any relative path before calling these tools.
Piping network content directly to a shell is blocked. It executes untrusted remote code without any inspection.
Blocked: curl URL | bash, curl URL | sh, wget URL | bash, curl URL | sudo bash
Safe alternative: download first, inspect, then execute:
curl -o /tmp/script.sh URL
cat /tmp/script.sh # inspect
bash /tmp/script.sh # execute if safe
Before making a git commit in the hooks daemon repository, this handler advises verifying that the daemon can restart successfully with the current code changes. This is advisory — it adds context but does not block the commit.
Why: Unit tests alone don't catch import errors. A handler that fails to import silently disables protection without any test-time error. Daemon restart is the definitive check.
Run before committing (in this repo only):
$PYTHON -m claude_code_hooks_daemon.daemon.cli restart then verify status shows RUNNING.
chmod 777 and other world-writable permission commands are blocked. Overly permissive file permissions are a security vulnerability.
Blocked: chmod 777, chmod 666, chmod a+w, chmod o+w
Use least-privilege permissions instead:
- Executable scripts:
chmod 755(owner rwx, group/other rx) - Regular files:
chmod 644(owner rw, group/other r) - Private files:
chmod 600(owner rw only)
The following git commands are permanently blocked and will always be denied:
| Command | Reason |
|---|---|
git reset --hard |
Permanently destroys all uncommitted changes |
git clean -f |
Permanently deletes untracked files |
git checkout -- <file> |
Discards all local changes to that file |
git restore <file> |
Discards local changes (--staged is allowed) |
git stash drop |
Permanently destroys stashed changes |
git stash clear |
Permanently destroys all stashes |
git push --force |
Can overwrite remote history and destroy teammates' work |
git branch -D |
Force-deletes branch without checking if merged (lowercase -d is safe) |
git commit --amend |
Rewrites the previous commit — create a new commit instead |
If the user needs to run one of these, ask them to do it manually. Do not attempt to work around the block.
Safe alternatives: git stash (recoverable), git diff / git status (inspect first), git commit (save changes permanently first).
Writing code that silently swallows errors is blocked. All errors must be handled explicitly.
Blocked patterns (examples):
- Python: bare
exceptclauses with an empty body, catching and discarding all exceptions - Shell: redirecting stderr to
/dev/nullto silence failures,|| trueto suppress non-zero exit codes - JavaScript/TypeScript: empty
catchblocks that swallow exceptions - Go:
_ = err(discarding error return values without handling)
Required action: Handle errors explicitly — log them, return them to the caller, or propagate them. Silent error suppression masks bugs and makes debugging impossible.
gh issue view without --comments is blocked. Issue comments often contain critical context, clarifications, and updates not in the issue body.
Blocked: gh issue view 123, gh issue view 123 --repo owner/repo
Allowed: gh issue view 123 --comments, gh issue view 123 --json title,body,comments
If using --json, include comments in the field list instead of adding --comments.
Direct Write or Edit to package manager lock files is blocked. Lock files are generated artifacts; manual edits create checksum mismatches and broken dependency graphs.
Blocked files: composer.lock, package-lock.json, yarn.lock, pnpm-lock.yaml, Gemfile.lock, Cargo.lock, go.sum, Package.resolved, Pipfile.lock, and others.
Use package manager commands instead:
- PHP:
composer install/composer require package - Node:
npm install/yarn add package - Ruby:
bundle install/bundle add gem - Rust:
cargo add crate - Go:
go get module
Using Grep or Bash (grep/rg) to find class definitions, function signatures, or symbol references is blocked or redirected to LSP tools, which are faster and semantically accurate.
Prefer LSP tools for:
- Finding where a class or function is defined →
goToDefinition - Finding all usages of a symbol →
findReferences - Getting type information or documentation →
hover - Listing all symbols in a file →
documentSymbol - Searching symbols across the project →
workspaceSymbol
Grep/Bash grep is still appropriate for: text patterns in content, log searching, finding strings in config files.
Default mode (block_once): the first symbol-lookup grep in a session is denied with guidance; subsequent retries are allowed.
Writing a new .md file to an unrecognised location is blocked. Markdown files must be placed in project-configured allowed paths.
Common allowed locations: CLAUDE/, docs/, RELEASES/, CLAUDE/Plan/, root-level README.md, or any path matching the allowed_markdown_paths config.
Dependency directories: vendor/ (PHP) and node_modules/ (JS) are treated as implicit monorepos — each package is a sub-project where normal markdown rules apply (e.g. vendor/acme/lib/docs/guide.md is allowed, vendor/acme/lib/random/notes.md is blocked).
Plan file redirection: when track_plans_in_project is enabled, Claude Code planning mode writes are automatically redirected to the project's CLAUDE/Plan/ directory. Plan folders must follow the NNNN-description/ naming convention.
If you need a markdown file in a new location, add a pattern to allowed_markdown_paths in .claude/hooks-daemon.yaml.
If your project has sub-projects with their own docs/, CLAUDE/, etc., configure monorepo_subproject_patterns in .claude/hooks-daemon.yaml so normal rules apply within each sub-project.
Direct npm run and npx commands are blocked or advised against. Projects with llm: prefixed scripts in package.json should use those instead.
Why: llm: commands are configured for LLM-friendly output (no spinners, no colour codes, structured results).
Example: Use npm run llm:build instead of npm run build.
If no llm: commands exist in package.json, the handler operates in advisory mode (warns but does not block).
Commands piped to tail or head are blocked — piping truncates output and causes information loss.
Use a temp file instead:
# WRONG — blocked:
pytest tests/ 2>&1 | tail -20
# RIGHT — redirect to temp file:
pytest tests/ > /tmp/pytest_out.txt 2>&1
# Then read selectively if neededAllowed (whitelisted): grep, rg, awk, sed, jq, ls, cat, git log, git tag, git branch, and other cheap filtering commands.
Add to whitelist (if safe to pipe): set extra_whitelist in .claude/hooks-daemon.yaml under pipe_blocker.
Writing QA suppression directives into source files is blocked across all supported languages. Fix the underlying code issue instead.
Blocked annotation types (by language):
- Python:
noqadirectives,type: ignoreannotations - JavaScript/TypeScript:
eslint-disableinline directives - Go:
nolintdirectives (golangci-lint) - PHP:
phpstan-ignore,psalm-suppressannotations - Java/Kotlin:
@SuppressWarnings,@Suppressannotations - C#:
pragma warning disabledirectives - Rust:
allow(clippy::...)attributes on type-level items
Required action: Fix the code so QA passes without suppression. If a suppression is genuinely necessary, ask the user to add it manually — this signals a conscious decision rather than a shortcut.
Writing code that contains security antipatterns is blocked across all supported languages. Fix the code to use safe patterns instead.
Blocked categories:
- SQL injection: building queries via string concatenation (use parameterised queries)
- Command injection: passing unvalidated input to subprocess (use argument lists)
- Hardcoded credentials: API keys, passwords, tokens embedded in source code
- Weak cryptography: MD5 or SHA1 for password hashing (use bcrypt/argon2)
- Path traversal: unvalidated user input used in file paths
Supported languages: Python, JavaScript/TypeScript, Go, PHP, Ruby, Java, Kotlin, C#, Rust, Swift, Dart.
sed is blocked because Claude gets sed syntax wrong and a single error can silently destroy hundreds of files with no recovery possible.
Blocked:
sed -i/sed -e(in-place file editing via Bash tool)grep -rl X | xargs sed -i(mass file modification)- Shell scripts (
.sh/.bash) written via Write tool that containsed
Allowed (read-only, no file modification):
cat file | sed 's/x/y/' | grep z(pipeline transforming stdout only)sedmentioned in commit messages, PR bodies, or.mddocumentation files
Use instead:
Edittool — safe, atomic, verifiable- Parallel Haiku agents with
Edittool for bulk changes across many files:- Identify all files to update
- Dispatch one Haiku agent per file
- Each agent uses the
Edittool (neversed)
Creating a production source file is blocked until a corresponding test file exists.
TDD workflow (required):
- Create the test file first (e.g.
tests/unit/handlers/test_my_handler.py) - Write failing tests — RED phase
- Create the source file and implement until tests pass — GREEN phase
- Refactor — REFACTOR phase
Supported languages: Python, Go, JavaScript/TypeScript, PHP, Rust, Java, C#, Kotlin, Ruby, Swift, Dart
Test file locations checked (any satisfies the block):
- Separate mirror:
tests/unit/{subdir}/test_{module}.py - Collocated:
{source_dir}/{module}.test.ts(JS/TS projects) - Test subdirectory:
{source_dir}/__tests__/{module}.test.ts
Allowed through without blocking: vendor dirs, node_modules, build outputs, generated files, and file extensions not in the supported language list.
Writing ephemeral or session-specific content to CLAUDE.md or README.md is blocked. These files should contain only stable instructions, not implementation logs or session state.
Blocked content types:
- Timestamps and ISO dates
- Status emoji followed by completion words (e.g. checkmark + 'Done')
- Implementation log sentences ('created the file X', 'added the class Y')
- Test output counts ('3 tests passed')
- LLM summary section headings ('## Summary', '## Key Points')
Content inside markdown code blocks is exempt from validation.
cp, mv, and rsync operations that move files from a worktree directory (untracked/worktrees/ or .claude/worktrees/) into the main repo (src/, tests/, config/) — or vice versa — are blocked.
Worktrees are isolated branches. Cross-copying corrupts that isolation and can silently overwrite in-progress work.
Allowed: operations within the same worktree branch. To merge changes: use git merge or git cherry-pick instead.
gh pr view without --comments is blocked. PR comments often contain review feedback, reviewer requests, and decisions not in the PR body.
Blocked: gh pr view 123, gh pr view 123 --repo owner/repo
Allowed: gh pr view 123 --comments, gh pr view 123 --json title,body,comments
If using --json, include comments in the field list instead of adding --comments.
git stash, git stash push, and git stash save are blocked. git stash pop, git stash apply, git stash list, and git stash show are always allowed.
Why: stashes get forgotten, lost, and block git pull. Use git commit -m 'WIP: ...' instead — WIP commits are acceptable.
Escape hatch (when commit truly won't work):
MUST_STASH_BECAUSE="explain why"; git stash
Configure via handlers.pre_tool_use.git_stash.options.mode: warn for advisory-only mode.
Writing or editing files under system paths (/etc/, /var/, /usr/, /opt/, /root/, /home/) is blocked. These are deployed files managed by Ansible.
Edit the project source instead:
/etc/foo→files/etc/foo/var/local/foo→files/var/local/foo/usr/bin/foo→files/usr/bin/foo
Then deploy via Ansible playbook.
Direct package management, service management, and system configuration commands are blocked. All system changes must go through Ansible playbooks.
Blocked: dnf install, systemctl enable/start/stop, gsettings set, useradd, pip install (system), npm install -g, flatpak install
Allowed (read-only queries): dnf info/list/search, systemctl status, gsettings get, flatpak list
Use instead: Create/update Ansible playbook in playbooks/imports/ and deploy with ansible-playbook.
After every Write or Edit of a .md or .markdown file, the content is re-formatted via mdformat + mdformat-gfm so that table pipes are aligned and column widths are consistent. The handler is non-terminal and advisory — it never blocks, it just rewrites the file on disk.
What changes:
- Table pipes are aligned vertically and delimiter rows widened to match cell widths.
- Ordered lists keep consecutive numbering (
1.2.3.). ---thematic breaks are preserved (mdformat's 70-underscore default is post-processed back).- Asterisks in table cells are escaped (
*→\*) as required by GFM.
Ad-hoc formatting of existing files:
$PYTHON -m claude_code_hooks_daemon.daemon.cli format-markdown <path>
On every new session this handler audits hook configuration across .claude/settings.json and .claude/settings.local.json. When it reports issues, fix them — do not ignore the warning.
- All hooks live in
settings.json. That file is tracked in version control, visible to teammates, and is the single source of truth for the daemon. settings.local.jsonmust contain ZEROhooksentries. It exists for per-developerpermissionsand IDE state only. Ahooksblock there is either (a) invisible to the rest of the team, or (b) duplicated withsettings.json— in which case the hook fires twice per event.- Hook commands must invoke the daemon wrapper. Every registered command must end with
/.claude/hooks/{event}. Anything else (inline Python, custom shell scripts, bespoke paths) is a legacy setup that bypasses the daemon entirely.
- Hooks in
settings.local.json: move eachhooksentry tosettings.json, then delete thehookskey fromsettings.local.json. Confirm no duplicates remain. - Legacy-style commands: replace them with a project-level handler. Run
$PYTHON -m claude_code_hooks_daemon.daemon.cli init-project-handlersto scaffold.claude/project-handlers/, port the logic into a handler class, then restore the daemon wrapper insettings.json. The daemon will auto-discover the new handler on restart. - Missing hooks: the daemon's installer writes the full set. If any are missing, re-run
install.pyor manually add the missing{event_name}entry pointing at"$CLAUDE_PROJECT_DIR"/.claude/hooks/{bash-key}. - Duplicate hooks: a hook registered in both files fires twice. Keep the
settings.jsonentry, delete fromsettings.local.json.
Before stopping, prefix your final message with STOPPING BECAUSE: followed by a clear reason:
STOPPING BECAUSE: all tasks complete, QA passes, daemon restart verified.
Why: The stop hook enforces intentional stops. Stopping without an explanation triggers an auto-block that asks you to explain or continue.
Alternatives:
STOPPING BECAUSE: <reason>— stops cleanly with explanation- Continue working — no need to stop unless all work is genuinely complete
Do NOT:
- Stop mid-task without explanation
- Ask confirmation questions and then stop (the hook auto-continues those)
- Use
AUTO-CONTINUEunless you intend to keep working indefinitely
Before asking a question, evaluate it critically:
- Tautological/rhetorical questions with obvious answers ("Should I continue?", "Would you like me to proceed?") — do NOT ask, just do it
- Errors with a clear next step ("The test failed, should I fix it?") — do NOT ask, just fix it
- Genuine choice questions where all options are valid ("Which of A, B, or C should we use?") — these deserve a response. Use
STOPPING BECAUSE: need user inputand ask your question