Claude Code Settings
Table of Contents
Summary
This file is the single authoritative source for .claude/settings.json.
It follows the literate programming style: every permission entry is
accompanied by prose naming the operations it enables, the concrete
command variations it covers, and the security reasoning behind the
grant. Do not edit .claude/settings.json directly; regenerate it with
compass build --direct settings. Entries are kept to the minimum
necessary; each wildcard is chosen to cover only the intended surface.
How to regenerate
Via the Build pillar in --direct mode — Emacs only, no cmake/vcpkg, so
it works in a light environment (the skills alias recreates the other
generated half of .claude/):
compass build --direct settings # .claude/settings.json compass build --direct settings skills # recreate all of .claude/
Or via the full-environment CMake target:
cmake --build --preset linux-clang-debug-ninja --target deploy_settings
Both invoke projects/ores.lisp/src/ores-build-settings.el, which tangles this file to
.claude/settings.json. Requires Emacs on PATH. Must be run outside the
sandbox (the ores-babel.el project detection needs a full environment;
the CMake path also needs compiler detection during reconfiguration); use
dangerouslyDisableSandbox: true on the Bash tool call.
Master settings block
The master block assembles the final JSON from noweb references defined
in the sections below. It carries no :tangle directive; the output
path (.claude/settings.json under the repo root) is injected by
projects/ores.lisp/src/ores-build-settings.el via
org-babel-default-header-args:json using the ores/repo-root
variable from projects/ores.lisp/src/ores-babel.el.
Sections are separated by a trailing comma in the reference line; the
last section in each array carries no comma.
{
"autoMemoryEnabled": false,
"permissions": {
"allow": [
<<perm-files>>,
<<perm-build>>,
<<perm-sql>>,
<<perm-compass>>,
<<perm-codegen>>,
<<perm-tests>>,
<<perm-vcs>>,
<<perm-gh-pr>>,
<<perm-web>>,
<<perm-shell>>
],
"deny": [
<<perm-deny>>
]
},
"sandbox": {
<<sandbox-network>>,
<<sandbox-filesystem>>,
<<sandbox-excluded>>
},
"hooks": {
<<hooks-pretooluse>>
}
}
Top-level settings
autoMemoryEnabled
Set to false to disable Claude Code's built-in harness auto-memory
system, which otherwise writes MEMORY.md and per-topic *.md files
under ~/.claude/projects/<hash>/memory/. ORE Studio uses its own
version-controlled memory system under doc/llm/memory/ (see Project
memory); running the harness system in parallel creates a second,
unreviewed memory source that can contradict or duplicate the org
system. Disabling it ensures all durable LLM memory is in the repo
and tracked by git.
Permissions
Permission strings use glob wildcards (*), not regular expressions.
* matches any sequence of characters including spaces but not /,
so it covers arguments but not sub-paths. A space before * enforces
a word boundary: Bash(make *) matches make -j2 foo but not
make_something. Each subsection documents the surface covered and
the reason the grant is necessary.
File access
Edit(**) and Write(**) grant read/write access to every file in the
project tree without hardcoding an absolute path. Claude Code resolves
permission globs relative to the directory containing .git — the
project root — at startup. This means the same committed
settings.json works across all checkouts regardless of where they
live on disk (/mnt/development/..., /home/user/..., etc.).
** matches zero or more path segments including directory separators,
so Edit(**) covers foo.txt, projects/ores.risk/src/foo.cpp, and
any other path depth. This replaces the previous per-subtree entries
(Edit(projects/**), Edit(doc/**), etc.) with a single grant that
covers the whole tree.
Read(*) is not included here because Claude Code does not require an
explicit allow entry for reads; they are permitted by default unless
the sandbox filesystem deny-list blocks them.
"Edit(**)", "Write(**)"
Build tools
The build system requires make, cmake, and (for non-compilation
deploy targets) emacs in batch mode. All three are invoked with
varying flags and targets depending on the operation.
Bash(make *) is broader than a path-restricted form but narrower
than an unconditional build allow. Any make invocation is a
compilation or deployment action; no make-based attack vector exists
in this project context beyond unintended compilation.
Bash(cmake *) covers both configuration (cmake --preset) and build
(cmake --build) steps. The wildcard is necessary because the preset
name and target name vary.
Bash(emacs -Q --script *) covers the deploy-family targets
(deploy_skills, deploy_settings, deploy_site) which invoke Emacs
in batch mode. The -Q flag disables user configuration; --script
restricts execution to the named file. Only scripts checked into the
repository are passed here.
Bash(sccache *) and Bash(ccache *) cover direct invocations of
whichever compiler cache the checkout is configured for (selected via
compass env configure --compiler-cache {sccache,ccache}; see
ORES_COMPILER_CACHE in .env): starting or stopping the sccache
daemon (sccache --start-server, sccache --stop-server), querying
statistics (sccache --show-stats, ccache -s), and manual cache
management (sccache --zero-stats, ccache -C). CMake invokes the
selected tool transparently as a compiler wrapper
(CMAKE_CXX_COMPILER_LAUNCHER); those subprocess invocations do not
require a separate entry, but direct calls from Claude do.
Common invocations:
make -C build/output/linux-clang-debug-make -j2 ores.utilitycmake --build --preset linux-clang-debug-ninja --target deploy_skillscmake --build --preset linux-clang-debug-ninja --target deploy_settingsemacs -Q --script projects/ores.lisp/src/ores-build-skills.elemacs -Q --script projects/ores.lisp/src/ores-build-settings.el./compass.sh build ores.utility.testssccache --start-serversccache --show-statsccache -s
"Bash(make *)", "Bash(cmake *)", "Bash(emacs -Q --script *)", "Bash(sccache *)", "Bash(ccache *)"
SQL scripts
Four scripts under projects/ores.sql/ manage the local development
database. Each is granted individually rather than via a directory
wildcard (e.g. Bash(projects/ores.sql/* *)) to prevent accidental
execution of other files in that tree.
compass db sql executes SQL against the database.
compass db recreate drops and rebuilds the entire schema.
validate_schemas.sh checks that the live schema matches expectations.
kill_db_connections.sh terminates stale connections before a recreate.
All four require dangerouslyDisableSandbox: true at the Bash tool
level because the sandbox blocks outbound connections to
localhost:5432. The sandbox.network entry below grants localhost
access, which may make the per-call override unnecessary once Claude
Code respects project-level sandbox overrides.
Note: never prepend PGPASSWORD on the command line; these scripts
source .env themselves.
Common invocations:
compass db sql -- -f projects/ores.sql/schema/foo_create.sqlcompass db recreateprojects/ores.sql/utility/validate_schemas.shprojects/ores.sql/utility/kill_db_connections.sh
"Bash(projects/ores.compass/compass.sh db *)", "Bash(projects/ores.sql/utility/validate_schemas.sh *)", "Bash(projects/ores.sql/utility/kill_db_connections.sh *)"
Compass
Compass is the developer toolkit purpose-built to simplify LLM
workflows: orientation (bearings, where, fleet), navigation
(search, list, show), agile authoring (add, story new,
task new), session continuity (journal), PR work (pr checks,
review list/reply/resolve) and environment provisioning (env).
CLAUDE.md's first instruction to every session is ./compass.sh
bearings, and project memory directs the LLM to prefer compass over
raw Unix tools and to use it for all agile artefacts.
A tool we mandate on every session must not pay a permission prompt
per invocation. The grant is deliberately blanket rather than
per-subcommand: compass's mutating surface is repo-local files (doc
scaffolds, .journal.org, .env) and branch creation, all of which
the session can already do unprompted via Edit=/=Write=/=git * — a
read-only subset would add ceremony exactly on the highest-frequency
flows (scaffolding, journaling) without adding safety. Compass is the
trusted, recipe-documented boundary; authorising the entry point
authorises its delegations (git, gh, codegen) through compass only.
All four invocation forms are covered: the root wrapper (what
CLAUDE.md instructs), the component script, each with and without the
./ prefix.
Invocation convention: always call compass using one of these four
relative forms from the project root. Never invoke it via its absolute
path (e.g. /mnt/development/.../compass.sh) — absolute paths do not
match any of the patterns below and will trigger a permission prompt.
When the Bash tool's working directory has drifted into a subdirectory
(e.g. after working in projects/ores.codegen), prepend a cd to the
repo root before calling compass:
cd /mnt/development/Development/OreStudio/ores_dev_brave_hopper && ./compass.sh add capture ...
"Bash(./compass.sh *)", "Bash(compass.sh *)", "Bash(./projects/ores.compass/compass.sh *)", "Bash(projects/ores.compass/compass.sh *)"
Code generation
codegen.sh is the main entry point for C++ and SQL
code generation via ores.codegen (regenerate, generate
subcommands). plantuml_er_generate.sh produces PlantUML ER diagrams.
generate_doc.sh is a legacy script being decommissioned — use
compass instead.
The grant is per-script rather than per-directory for the same reason as SQL: a directory wildcard would permit running arbitrary scripts in a large codegen tree.
Invocation convention: always call codegen using one of the relative
forms below from the project root — with or without the ./ prefix.
Never use an absolute path; absolute paths do not match the patterns
below and trigger a permission prompt.
Common invocations:
./projects/ores.codegen/codegen.sh regenerate --component refdata --address ores.cppprojects/ores.codegen/codegen.sh regenerate --component refdata --address ores.cppprojects/ores.codegen/codegen.sh generate --model path/to/model.json --address ores.cppprojects/ores.codegen/plantuml_er_generate.sh -i doc/plans/foo.org
"Bash(./projects/ores.codegen/codegen.sh *)", "Bash(projects/ores.codegen/codegen.sh *)", "Bash(./projects/ores.codegen/generate_doc.sh *)", "Bash(projects/ores.codegen/generate_doc.sh *)", "Bash(./projects/ores.codegen/plantuml_er_generate.sh *)", "Bash(projects/ores.codegen/plantuml_er_generate.sh *)"
Test execution
Tests are run either through CTest (which dispatches to individual
binaries) or by invoking binaries directly for focused runs. Test
binary paths follow the pattern
build/output/<preset>/publish/bin/ores.<component>.tests.
Bash(ctest *) covers all CTest-mediated runs including the rat
target (which runs all registered suites). The direct-binary pattern
Bash(build/output/*/publish/bin/ores.* *) uses two wildcards: the
first matches the preset directory name (e.g. linux-clang-debug-make)
without crossing / boundaries; the second matches the component name
suffix and any arguments (filter tags, verbosity flags).
No wildcard is broad enough to execute arbitrary binaries outside the
publish/bin/ores.* namespace.
Common invocations:
ctest --test-dir build/output/linux-clang-debug-make -R ores.utilitybuild/output/linux-clang-debug-make/publish/bin/ores.utility.testsbuild/output/linux-clang-debug-make/publish/bin/ores.risk.tests "[generators]"
"Bash(ctest *)", "Bash(build/output/*/publish/bin/ores.* *)"
Version control
Bash(git *) covers all git subcommands. This is intentionally broad:
every git operation (read-only introspection, branching, staging,
committing, pushing, rebasing) shares the same prefix. Narrowing by
subcommand would add many entries without meaningfully reducing risk,
since all operations target the local repository or the authorised
remote.
Common invocations:
git add projects/ores.risk/src/domain/foo.cppgit commit -m "[risk] Add foo entity"git push --set-upstream origin feature/foogit rebase main
"Bash(git *)"
GitHub CLI — pull requests
The safe gh pr subcommands are granted individually. Four subcommands
are deliberately excluded and will always prompt for user confirmation:
| Excluded | Reason |
|---|---|
gh pr close |
Closes a PR; discards in-progress work. |
gh pr revert |
Creates a revert PR against a merged commit; external and visible. |
gh pr lock |
Moderation action; unusual enough to warrant a prompt. |
gh pr unlock |
Same as above. |
All other subcommands are either read-only (view, list, status,
checks, diff) or routine workflow actions (create, edit,
comment, review, ready, reopen, update-branch, checkout,
merge). Note: gh pr merge * is intentionally broad — it covers
--squash, --rebase, --admin, and --delete-branch without
prompting. This is an explicit trade-off: Claude Code is trusted to
merge its own reviewed PRs autonomously. The irreversibility of merge is
accepted; user confirmation is reserved for the destructive subcommands
above (close, revert, lock).
These commands require dangerouslyDisableSandbox: true because gh
authenticates via the OS keyring, which is not accessible inside the
sandbox. Raw git push/fetch operations need
SSH_AUTH_SOCK exported first (see
Set SSH_AUTH_SOCK for git operations); gh uses its own token, not
the SSH agent.
Common invocations:
gh pr create --title "[risk] Add foo" --body "..."gh pr checks 947gh pr view 947 --commentsgh pr edit 947 --body "..."gh api repos/OreStudio/OreStudio/pulls/947/commentsgh api graphql -f query'…'=
"Bash(gh pr view *)", "Bash(gh pr list *)", "Bash(gh pr status *)", "Bash(gh pr checks *)", "Bash(gh pr diff *)", "Bash(gh pr create *)", "Bash(gh pr edit *)", "Bash(gh pr comment *)", "Bash(gh pr review *)", "Bash(gh pr ready *)", "Bash(gh pr reopen *)", "Bash(gh pr update-branch *)", "Bash(gh pr checkout *)", "Bash(gh pr merge *)", "Bash(gh run *)", "Bash(gh api *)"
Web access
WebFetch(domain:*) allows fetching from any domain. The sandbox
network allowlist (see below) is the actual security boundary; the
permission entry controls only whether Claude Code prompts. Domains
not in the network allowlist remain unreachable regardless.
WebSearch enables web search. No domain restriction is applied here;
the search provider is fixed by the Claude Code runtime.
"WebFetch(domain:*)", "WebSearch"
Denied tools
Agent (subagent spawning — Explore, general-purpose, and the other
built-in agent types) is denied. We have been hitting reliability
problems with delegated subagents on this project: work that looked
complete in the agent's summary sometimes wasn't actually done the way
described, and there is very little visibility into what a subagent
did while it ran — its intermediate tool calls, reasoning, and any
missteps are hidden behind the single summary message it returns.
When something goes wrong inside a subagent, the failure surfaces
several turns later as a confusing downstream symptom instead of at
the point it happened, and there is no transcript to inspect to find
the root cause. Until subagent visibility improves, all substantive
work should happen in the main session where every tool call is
visible and reviewable. This blocks every agent type reachable via
the Agent tool (claude, Explore, general-purpose, Plan,
etc.) — none of them are exempt. It does not affect the Workflow
tool, which is gated separately by explicit opt-in.
"Agent"
Shell utilities
Read-only and data-processing utilities used in analysis pipelines, build inspection, and document generation. Each covers a standard POSIX tool.
Permission rules evaluate compound commands per subcommand: every
piece of a && b or a | b must match an allow rule or the whole
invocation prompts. In practice almost every compass or git command
arrives wrapped in a pipeline — ./compass.sh list --type recipe |
head, git log --oneline | tail -5 — so an uncovered head or
tail reintroduces a prompt on the highest-frequency flows even
though the primary command is authorised. The set below covers the
pipeline vocabulary those flows actually use.
sed and awk are deliberately not listed: both can execute
arbitrary shell commands (awk 'BEGIN {system(...)}', GNU sed's
e command), so auto-approving them would turn a text-processing
grant into an unprompted arbitrary-execution grant. They prompt on
use; prefer the Read=/=Edit tools for file inspection and
modification. python3 * is required for ad-hoc analysis scripts and
codegen helpers (src/doc_generate.py, etc.). java * is required
for PlantUML, which is invoked as a JAR.
Common invocations:
find . -name "*.org" -newer doc/foo.orggrep -r "workspace_id" projects/ores.workspace/./compass.sh list --type capture | head -50python3 projects/ores.codegen/src/doc_generate.py --helpjava -jar /usr/share/plantuml/plantuml.jar -checkonly foo.pumlwc -l build/output/linux-clang-debug-make/publish/bin/ores.utility.tests
"Bash(find *)", "Bash(grep *)", "Bash(python3 *)", "Bash(java *)", "Bash(echo *)", "Bash(wc *)", "Bash(sort *)", "Bash(uniq *)", "Bash(head *)", "Bash(tail *)", "Bash(cat *)", "Bash(ls *)", "Bash(cut *)", "Bash(tr *)", "Bash(diff *)"
Sandbox overrides
The sandbox restricts filesystem writes and outbound network
connections beyond the defaults. These entries extend the allowlist
at the project level; they are applied in addition to (not instead
of) the global sandbox configuration in ~/.claude/settings.json.
Network
Grants outbound connections to localhost, required for PostgreSQL
(port 5432) access from SQL scripts. Without this entry, psql calls
fail with a connection refused error inside the sandbox and the
scripts must be run with dangerouslyDisableSandbox: true.
mcraveiro.github.io hosts the Dogen documentation and MASD reference
material used when writing knowledge documents and literate template
prose; fetching it during documentation tasks must not prompt.
github.com and api.github.com serve the gh CLI (PR create,
checks, merge, review) — the permission allow-list already grants
gh * commands, so their API traffic must not prompt either. git
itself does not need these entries: it is excluded from the sandbox
(see the Hooks section), so its pushes and fetches bypass the network
allowlist entirely.
"network": {
"allowedHosts": ["localhost", "mcraveiro.github.io", "github.com", "api.github.com"]
}
Filesystem
Extends the write allowlist to include the sccache=/=ccache cache
directories, both under the shared SSD checkout disk
(/mnt/development/.cache/; see .env's ORES_COMPILER_CACHE). The
sandbox otherwise blocks writes there, causing cache misses and
forcing builds to recompile from scratch or requiring
dangerouslyDisableSandbox: true.
Two entries are required per tool: the directory path itself, and a
/** glob authorising all files and subdirectories within it (object
cache shards, the stats file, the server log, etc.). Without the
/** glob, the tool receives EPERM when writing any cache object
because the allowlist matches only the directory path, not its
contents.
"filesystem": {
"allowWrite": [
"/mnt/development/.cache/sccache",
"/mnt/development/.cache/sccache/**",
"/mnt/development/.cache/ccache",
"/mnt/development/.cache/ccache/**"
]
}
Excluded commands
Commands listed here never attempt to run inside the sandbox; they go straight to the regular permission flow (where the allow rules above auto-approve them). This kills the failure → retry-unsandboxed → prompt loop without widening what is authorised: exclusion changes where a command runs, not whether it is allowed.
Git needs this because linked worktrees resolve their gitdir into the
primary checkout (OreStudio.remote/.git/worktrees/<name>), which sits
outside the sandbox write allowlist — git fetch, add and commit
fail in-sandbox on the index lock and FETCH_HEAD, then prompt on every
unsandboxed retry. Compass needs it for the same reason one level up:
its highest-value verbs (pr create, pr sync, capture promote,
task start, task done) spawn git internally, so a sandboxed
compass run inherits the same gitdir failures. All four compass
invocation forms from the permission grant are mirrored here.
"excludedCommands": [ "./compass.sh *", "compass.sh *", "./projects/ores.compass/compass.sh *", "projects/ores.compass/compass.sh *", "git *" ]
Hooks
Deny piped or redirected compass invocations
Permission allow/deny rules cannot express this rule: they evaluate a
compound command per subcommand (see the Shell utilities rationale
above), so a pipeline like ./compass.sh build --direct site | tail
-30 is checked as two independent, individually-authorised pieces
(compass.sh build ... and tail -30) — there is no way for a glob
rule to see "compass, piped into tail" as a single shape to deny, nor
to distinguish it from an unrelated, perfectly fine tail -f
/tmp/some.log. A PreToolUse hook is the only mechanism that
inspects the full command string as written, so it is what enforces
CLAUDE.md's "never pipe or redirect a compass command" rule: every
compass command that produces meaningful output prints its own
well-known log-file path (📝 Build output: /tmp/<label>_<target>_build.log
(tail -f to follow); see compass.py's _build_log_path=/=_direct_build_log_path) —
watching that reported file with a separate, standalone tail is the
supported way to follow progress.
The hook fires on every Bash tool call, matches the command against
all four invocation forms compass is granted under (see Compass
above) plus the raw script name, and denies (exit code 2, message on
stderr) only when that same command string also contains a pipe
(|) or a redirect (>, which also matches 2>&1 and >>). A
denial's stderr becomes the tool result the agent sees, so the
message doubles as the correction: it names the rule and repeats what
to do instead, rather than the agent having to infer that from a bare
rejection.
Fail open, not closed, if the script is missing. The hook is a
project-tracked file — it can legitimately be absent on a given
git checkout (an older branch predating this task, a worktree mid
rebase, this same PR's own commit not yet checked out). Claude Code
treats a hook command that itself fails to execute (e.g. python3
reporting "No such file or directory") as a hard block of the
Bash tool entirely — every subsequent command, compass or not,
gets denied with no way to self-correct, since the correcting action
itself requires a Bash call. That is a materially worse failure
mode than "the piping rule goes briefly unenforced," so the actual
hook invocation is wrapped in a shell guard that checks the script
exists before running it, exiting 0 (allow) if it doesn't rather than
letting the missing-file error propagate as a block:
sh -c 'f=projects/ores.compass/src/deny_piped_compass_hook.py; if [ -f "$f" ]; then python3 "$f"; else exit 0; fi'
Deliberately an if=/=then=/=else, not the shorter [ -f "$f" ] &&
python3 "$f" || exit 0: in an A && B || C chain, C runs if
either A or B fails, so a genuine deny (python3 exiting 2)
would also satisfy || exit 0 and get silently swallowed into an
allow — the exact bug this guard exists to avoid, just moved one
level up. With if=/=then=/=else, the compound command's exit
status is whatever the taken branch's last command produced:
python3 "$f"'s own 0/2 when the file exists, or the explicit
exit 0 only when the [ -f ] test itself is what failed. The
literal string tangled into .claude/settings.json (below) is this
same guard, JSON-escaped.
"""PreToolUse hook: deny compass.sh invocations that pipe or redirect their output. See CLAUDE.md's "Never pipe or redirect a compass command" rule and doc/llm/claude_code_settings.org's Hooks section. Compass build/deploy commands print their own well-known log-file path for exactly this purpose; tailing that file separately is the supported way to follow progress instead. """ import json import re import sys COMPASS_RE = re.compile( r"(?:^|[\s&;(])(?:\./)?(?:projects/ores\.compass/)?compass\.sh(?:\s|$)" ) # A lone "|" (pipe) not adjacent to another "|" -- excludes the "||" # logical-OR operator (e.g. `compass.sh build || echo failed`, a normal # error-handling idiom that pipes nothing) while still catching a real # single-pipe pipeline. Any ">" is a redirect (">", ">>", "2>&1" all # contain it) and has no equivalent non-redirect meaning to exclude. REDIRECT_RE = re.compile(r"(?<!\|)\|(?!\|)|>") DENIAL_MESSAGE = ( "Never pipe or redirect a compass command (no `|`, no `>`, no " "`2>&1`) -- run it bare. Compass build/deploy commands print their " "own well-known log-file path (e.g. \"\U0001F4DD Build output: " "/tmp/<label>_<target>_build.log (tail -f to follow)\"). To watch " "progress, tail THAT reported file as a separate, standalone " "command (e.g. `tail -f <path>`) -- never by piping the compass " "invocation itself. See CLAUDE.md.\n" ) def main() -> int: try: data = json.load(sys.stdin) except (ValueError, json.JSONDecodeError): return 0 if not isinstance(data, dict) or data.get("tool_name") != "Bash": return 0 command = data.get("tool_input", {}).get("command", "") if COMPASS_RE.search(command) and REDIRECT_RE.search(command): sys.stderr.write(DENIAL_MESSAGE) return 2 return 0 if __name__ == "__main__": sys.exit(main())
""" Tests for the piped/redirected-compass PreToolUse hook. Run with: python -m pytest projects/ores.compass/tests/test_deny_piped_compass_hook.py -v No live database or file system access required. """ import io import sys from pathlib import Path # Allow importing from the src directory without installing the package. sys.path.insert(0, str(Path(__file__).parent.parent / "src")) import deny_piped_compass_hook as hook def run(command, tool_name="Bash", monkeypatch=None): """Drive hook.main() with a synthetic PreToolUse payload on stdin, returning (exit_code, stderr_text).""" payload = {"tool_name": tool_name, "tool_input": {"command": command}} import json monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload))) stderr = io.StringIO() monkeypatch.setattr(sys, "stderr", stderr) return hook.main(), stderr.getvalue() def test_bare_compass_allowed(monkeypatch): code, _ = run("./compass.sh build --direct site", monkeypatch=monkeypatch) assert code == 0 def test_piped_compass_denied(monkeypatch): code, msg = run("./compass.sh build --direct site 2>&1 | tail -30", monkeypatch=monkeypatch) assert code == 2 assert "Never pipe or redirect" in msg def test_redirected_compass_denied(monkeypatch): code, _ = run("./compass.sh build > out.log", monkeypatch=monkeypatch) assert code == 2 def test_logical_or_compass_allowed(monkeypatch): """`||` is error handling, not a pipe -- must not be denied.""" code, _ = run("./compass.sh build --direct site || echo failed", monkeypatch=monkeypatch) assert code == 0 def test_all_four_invocation_forms_denied_when_piped(monkeypatch): for prefix in ( "./compass.sh", "compass.sh", "./projects/ores.compass/compass.sh", "projects/ores.compass/compass.sh", ): code, _ = run(f"{prefix} build | tail", monkeypatch=monkeypatch) assert code == 2, prefix def test_unrelated_pipe_allowed(monkeypatch): code, _ = run("git log --oneline | head -5", monkeypatch=monkeypatch) assert code == 0 def test_unrelated_logical_or_allowed(monkeypatch): code, _ = run("git fetch || echo failed", monkeypatch=monkeypatch) assert code == 0 def test_non_bash_tool_allowed(monkeypatch): code, _ = run("./compass.sh build | tail", tool_name="Read", monkeypatch=monkeypatch) assert code == 0 def test_non_dict_json_does_not_crash(monkeypatch): monkeypatch.setattr(sys, "stdin", io.StringIO("[1, 2, 3]")) assert hook.main() == 0 def test_null_json_does_not_crash(monkeypatch): monkeypatch.setattr(sys, "stdin", io.StringIO("null")) assert hook.main() == 0 def test_malformed_json_does_not_crash(monkeypatch): monkeypatch.setattr(sys, "stdin", io.StringIO("not json")) assert hook.main() == 0
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "sh -c 'f=projects/ores.compass/src/deny_piped_compass_hook.py; if [ -f \"$f\" ]; then python3 \"$f\"; else exit 0; fi'"
}
]
}
]
See also
- How do I update Claude Code permissions? — full workflow for adding or fixing an allow-list entry.
- How do I deploy the settings? — redeploy after editing.
- How do I deploy the skills? — parallel deploy target.
- CMake setup — preset names and output layout.
- ores.sql — service table isolation and SQL script conventions.