Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions .claude/hooks/bash-chain-guard.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
#!/usr/bin/env node
// PreToolUse hook: blocks command chaining in Bash tool calls
// Detects &&/|| which violate AGENTS.md: "execute exactly one command per tool call"
// Enforces AGENTS.md: "execute exactly one command per tool call"
// Quoted spans and backslash-escapes are replaced with a space before scanning, so a `;` inside a
// string literal (or the trailing `\;` of `find -exec`) is not mistaken for a chain. Replacing with
// a space rather than deleting keeps tokens separate, so `printf hi >\x&` cannot collapse to
// `printf hi >&` and hide the backgrounding `&` behind the redirection lookbehind.
// Once https://github.com/anthropics/claude-code/issues/16561 is resolved, this should no longer be necessary
// Improvements based on https://github.com/iandunn/dotfiles/blob/main/claude/hooks/block-chained-commands.js

const CHAIN_OPERATORS = /&&|\|\||;/; // a single `|` is a pipe, which is allowed
Comment thread
ejfine marked this conversation as resolved.
const BACKGROUND_AMPERSAND = /(?<![<>])&(?!>)/; // a lone `&` backgrounds the command, but `2>&1` and `&>file` are redirections
Comment thread
ejfine marked this conversation as resolved.

function stripQuotedAndEscaped(command) {
return command.replace(/\\.|'[^']*'|"(?:\\.|[^"\\])*"/g, " ");
}
Comment thread
ejfine marked this conversation as resolved.

let input = "";
process.stdin.on("data", (chunk) => {
Expand All @@ -11,15 +23,24 @@ process.stdin.on("end", () => {
try {
const data = JSON.parse(input);
const command = (data.tool_input?.command || "").trim();
const scannable = stripQuotedAndEscaped(command);

if (/\s(&&|\|\||;)\s|\s&(\s|$)/.test(command)) {
if (CHAIN_OPERATORS.test(scannable)) {
process.stderr.write(
"AGENTS.md violation: never chain commands with &&, ||, ;, or &. " +
"AGENTS.md violation: never chain commands with &&, ||, or ;. " +
"Run one command per tool call. " +
"Use cd as a separate prior tool call instead of cd && ...",
);
process.exit(2);
}

if (BACKGROUND_AMPERSAND.test(scannable)) {
process.stderr.write(
"AGENTS.md violation: never background a command with &. " +
"Use the Bash tool's run_in_background option instead.",
);
process.exit(2);
}
} catch (e) {
// Silent fail — never block on hook errors
}
Expand Down
20 changes: 20 additions & 0 deletions .claude/settings/models.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* ============== WARNING ==============================================================================
* File is managed by copier template: gh:LabAutomationAndScreening/copier-base-template.git
* See .config/.copier-managed-files.json for details.
*
* You are welcome to make changes to this file in your repo if they are custom to your project,
* but if the change should be shared with other projects, please backport it to the template repo.
* =====================================================================================================
*/
{
"availableModels": [
"sonnet",
"opus",
"haiku",
"claude-opus-5",
"claude-opus-4-8",
"claude-sonnet-5",
"claude-sonnet-4-6"
]
}
1 change: 1 addition & 0 deletions .claude/settings/permissions/bash.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
"Bash(just *)",
"Bash(lsof *)",
"Bash(test *)",
"Bash(xxd)", // this invocation can be used to read, but not to write (so no " *" after it)
"Bash(zk *)",
// Node.js
"Bash(pnpm list *)",
Expand Down
3 changes: 3 additions & 0 deletions .claude/skills/address-pr-comments/fetch-pr-comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def gh_rest(*args: str) -> Any: # noqa: ANN401 — return type is genuinely unk
["gh", *call_args], # noqa: S607 — gh is expected on PATH
capture_output=True,
text=True,
encoding="utf-8",
check=True,
timeout=GH_TIMEOUT_SECONDS,
)
Expand All @@ -91,6 +92,7 @@ def gh_graphql(query: str, variables: dict[str, str | int]) -> Any: # noqa: ANN
args,
capture_output=True,
text=True,
encoding="utf-8",
check=True,
timeout=GH_TIMEOUT_SECONDS,
)
Expand All @@ -105,6 +107,7 @@ def fetch_current_user_login() -> str:
["gh", "api", "user", "--jq", ".login"], # noqa: S607 — gh is expected on PATH
capture_output=True,
text=True,
encoding="utf-8",
check=True,
timeout=GH_TIMEOUT_SECONDS,
)
Expand Down
2 changes: 1 addition & 1 deletion .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ reviews:
- path: "**/vendor_files/**"
instructions: "These files came from a vendor and we're not allowed to change them. Refer to it if you need to understand how the main code interacts with it, but do not make comments about it."
- path: "**/*.py"
instructions: "Never express concerns about potential ruff rule violations, even those rules mentioned in other agent instructions; a pre-commit hook already runs a ruff check. Do not warn about unnecessary super().__init__() calls; pyright prefers those to be present. Do not warn about missing type hints; a pre-commit hook already checks for that."
instructions: "Never express concerns about potential ruff rule violations, even those rules mentioned in other agent instructions; a pre-commit hook already runs a ruff check. Do not warn about unnecessary super().__init__() calls; pyrefly prefers those to be present. Do not warn about missing type hints; a pre-commit hook already checks for that."
- path: "**/.copier-answers.yml"
instructions: "Do not comment about the `_commit` value needing to be a clean release tag. A CI job will fail if that is not the case."
- path: ".config/.copier-managed-files.json"
Expand Down
2 changes: 1 addition & 1 deletion .config/.copier-answers.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Changes here will be overwritten by Copier
_commit: v0.0.140
_commit: v0.0.151
_src_path: gh:LabAutomationAndScreening/copier-base-template.git
copier_answers_directory: .config
description: Copier template for creating Python libraries and executables
Expand Down
6 changes: 4 additions & 2 deletions .config/.copier-managed-files.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
".claude/package.json",
".claude/settings/basics.jsonc",
".claude/settings/hooks.jsonc",
".claude/settings/models.jsonc",
Comment thread
ejfine marked this conversation as resolved.
".claude/settings/permissions/additional-dirs.jsonc",
".claude/settings/permissions/bash.jsonc",
".claude/settings/permissions/read.jsonc",
Expand Down Expand Up @@ -101,7 +102,6 @@
"copier_template_resources/vcrpy_fixtures.py",
"extensions/context.py",
"pyproject.toml",
"pyrightconfig.json",
"pytest.ini",
"scripts/delete_false_positive_rej.py",
"sh.bat",
Expand Down Expand Up @@ -138,6 +138,7 @@
"template/.claude/package.json",
"template/.claude/settings/basics.jsonc",
"template/.claude/settings/hooks.jsonc",
"template/.claude/settings/models.jsonc",
"template/.claude/settings/permissions/additional-dirs.jsonc",
"template/.claude/settings/permissions/bash.jsonc",
"template/.claude/settings/permissions/read.jsonc",
Expand Down Expand Up @@ -195,10 +196,11 @@
"template/CHANGELOG.md",
"template/README.md.jinja",
"template/biome.jsonc",
"template/pyrightconfig.json",
"template/pytest.ini",
"template/scripts/delete_false_positive_rej.py",
"template/sh.bat",
"tests/__init__.py",
"tests/conftest.py",
"tests/copier_data/data1.yaml",
"tests/copier_data/data2.yaml",
"tests/unit/copier_tasks/__init__.py",
Expand Down
2 changes: 1 addition & 1 deletion .config/.coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ exclude_also =
raise NotImplementedError
raise AssertionError

# Don't complain about code specifically to help with pyright import resolution
# Don't complain about code specifically to help with pyrefly import resolution
if TYPE_CHECKING:

# Don't complain if non-runnable code isn't run:
Expand Down
2 changes: 1 addition & 1 deletion .config/pyrefly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ project-includes = [
"../tests"
]
search-path = [".."]
disable-project-excludes-heuristics = true # with this being enabled by default, pyrefly things the src is excluded because it's an editable local install
disable-project-excludes-heuristics = true # with this being enabled by default, pyrefly thinks the src is excluded because it's an editable local install
python-platform = "linux"
infer-with-first-use = false # more similar to pyright. must define the empty list type when instantiated
# "strict" enables the maximalist error set plus strict-callable-subtyping and
Expand Down
6 changes: 6 additions & 0 deletions .config/ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ exclude = [
line-length = 120
indent-width = 4

# resolved relative to the cwd where ruff runs (repo root under pre-commit), so this nests the cache under .config
cache-dir = ".config/.ruff_cache"

target-version = "py312"

[lint]
Expand Down Expand Up @@ -66,8 +69,11 @@ ignore = [
"N999", # Ignoring this since we are using jinja templates in the directory path which will eventually turn into a valid module name. # TODO: make a task in downstream templates to remove this
"S101", # We do not use the -o optimize flag, so it's fine to use `assert` in the main code. It's especially helpful for static typing
"SIM102", # Nested if statements can be better analyzed by coverage detectors than compound statements
"SIM103", # Explicit if/else branches let the coverage checker track both paths, unlike evaluating an expression
"SIM108", # Explicit if/else branches let the coverage checker track both paths, unlike a ternary
"SIM110", # Explicit if statements and loops let the coverage checker track both paths, unlike using `any` or `all` with a generator expression
"SIM114", # Using `or` statements to simplify this would confuse the coverage checker
"SIM401", # using dictionary getters to simplify this would inhibit the coverage checker from tracking both paths
"TD002", # Adding author names to TODOs erodes shared ownership of codebase. Git history provides information about who originally created the TODO if that information is vitally needed
"TD003", # Up to the author's judgement whether a TODO requires a link to an issue or not
"TID252", # Sometimes it makes sense to use relative imports, that's a judgement call for us, not ruff
Expand Down
12 changes: 6 additions & 6 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@
"extensions": [
// basic tooling
// "eamodio.gitlens@15.5.1",
"coderabbit.coderabbit-vscode@0.21.0",
"coderabbit.coderabbit-vscode@0.21.3",
"ms-vscode.live-server@0.5.2025051301",
"MS-vsliveshare.vsliveshare@1.0.5905",
"anthropic.claude-code@2.1.211",
"anthropic.claude-code@2.1.220",

// Python
"ms-python.python@2026.5.2026052901",
"meta.pyrefly@1.1.9002",
"meta.pyrefly@1.1.9003",
"ms-vscode-remote.remote-containers@0.414.0",
"charliermarsh.ruff@2026.60.0",
"charliermarsh.ruff@2026.68.0",

// Misc file formats
"bierner.markdown-mermaid@1.29.0",
Expand All @@ -49,7 +49,7 @@
// TODO: see if there's a way to specify different configurations for different folders
"ruff.configuration": "/workspaces/copier-python-package-template/.config/ruff-non-src.toml", // use the non-src configuration since it's less restrictive and won't show false positives and underline things
"python.pyrefly.configPath": "/workspaces/copier-python-package-template/.config/pyrefly.toml",
"prettier.configPath": ".config/.prettierrc",
"prettier.configPath": "/workspaces/copier-python-package-template/.config/.prettierrc",
"[jsonc][json][javascript][typescript][graphql][css][scss][html][vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
Expand All @@ -60,5 +60,5 @@
"initializeCommand": "sh .devcontainer/initialize-command.sh",
"onCreateCommand": "sh .devcontainer/on-create-command.sh",
"postStartCommand": "sh .devcontainer/post-start-command.sh"
// Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): d56eca75 # spellchecker:disable-line
// Devcontainer context hash (do not manually edit this, it's managed by a pre-commit hook): bf3441ae # spellchecker:disable-line
}
6 changes: 3 additions & 3 deletions .devcontainer/install-ci-tooling.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
import tempfile
from pathlib import Path

UV_VERSION = "0.11.25"
PNPM_VERSION = "11.9.0"
COPIER_VERSION = "==9.16.0"
UV_VERSION = "0.12.1"
PNPM_VERSION = "11.20.0"
COPIER_VERSION = "==9.17.0"
COPIER_TEMPLATE_EXTENSIONS_VERSION = "==0.3.3"
PRE_COMMIT_VERSION = "4.5.1"
GITHUB_WINDOWS_RUNNER_BIN_PATH = r"C:\Users\runneradmin\.local\bin"
Expand Down
15 changes: 3 additions & 12 deletions .github/workflows/hash_git_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,24 +96,15 @@ def compute_adler32(repo_path: Path, files: list[str], extra_paths: list[Path] |
if file_path.name == "uv.lock":
checksum = zlib.adler32(get_uv_lock_bytes_for_hashing(file_path), checksum)
else:
with file_path.open("rb") as f:
while True:
chunk = f.read(4096)
if not chunk:
break
checksum = zlib.adler32(chunk, checksum)
# Normalize CRLF to LF so the checksum is identical on Windows (autocrlf) and Linux CI checkouts.
checksum = zlib.adler32(file_path.read_bytes().replace(b"\r\n", b"\n"), checksum)
Comment thread
ejfine marked this conversation as resolved.
except IsADirectoryError:
# Ignore symlinks that on windows sometimes get confused as being directories
continue

for extra_path in sorted(extra_paths or []):
checksum = zlib.adler32(str(extra_path).encode("utf-8"), checksum)
with extra_path.open("rb") as f:
while True:
chunk = f.read(4096)
if not chunk:
break
checksum = zlib.adler32(chunk, checksum)
checksum = zlib.adler32(extra_path.read_bytes().replace(b"\r\n", b"\n"), checksum)

return checksum

Expand Down
28 changes: 3 additions & 25 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ repos:
.devcontainer/devcontainer.json|
.devcontainer/devcontainer-lock.json|
.*devcontainer.json|
.*pyrightconfig.json|
.*tsconfig.json|
.*biome.jsonc|
.*/generated/open[-_]api/.*|
Expand All @@ -109,7 +108,6 @@ repos:
.*generated/graphql.ts|
.*/generated/open[-_]api/.*|
.*/schema.graphql|
.*pyrightconfig\.json|
)$

- repo: https://github.com/rbubley/mirrors-prettier
Expand All @@ -123,7 +121,6 @@ repos:
.devcontainer/devcontainer.json|
.devcontainer/devcontainer-lock.json|
.*devcontainer.json|
.*pyrightconfig.json|
.*tsconfig.json|
.*/__snapshots__/.*|
.*.min.js|
Expand Down Expand Up @@ -167,7 +164,6 @@ repos:
.devcontainer/devcontainer.json|
.devcontainer/devcontainer-lock.json|
.*devcontainer.json|
.*pyrightconfig.json|
.*tsconfig.json|
)$
- id: check-yaml
Expand Down Expand Up @@ -198,7 +194,6 @@ repos:
.devcontainer/devcontainer.json|
.devcontainer/devcontainer-lock.json|
.*devcontainer.json|
.*pyrightconfig.json|
.*tsconfig.json|
)$

Expand Down Expand Up @@ -235,6 +230,9 @@ repos:
hooks:
- id: hadolint-docker
name: Lint Dockerfiles
# The hadolint pre-commit has a problem where it just always pulls the `latest` image at runtime.
# Explicitly pinning the image here to the same rev as the hook to patch it until https://github.com/hadolint/hadolint/pull/1177 gets resolved.
entry: ghcr.io/hadolint/hadolint:v2.14.0@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e hadolint
exclude: .*\.jinja$
description: Runs hadolint to lint Dockerfiles

Expand Down Expand Up @@ -285,26 +283,6 @@ repos:
- --rcfile=./.config/pylintrc.toml
verbose: true

- repo: local
hooks:
- id: pyright
name: pyright
entry: bash -c "uv run pyright"
files: '.+\.py$'
exclude: |
(?x)^(
.*/generated/graphql/.*|
.*/generated/open[-_]api/.*|
)$
# don't pass filenames else the command line sees them twice
pass_filenames: false
language: system
types: [python]
# use require_serial so that script is only called once per commit
require_serial: true
# print the number of files as a sanity-check
verbose: true

- repo: local
hooks:
- id: pyrefly
Expand Down
Loading