mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-15 07:39:28 +00:00
Compare commits
4 Commits
iris/fix-g
...
joaomdmour
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cbba7bd0a | ||
|
|
96cec9a08e | ||
|
|
04382bc9ba | ||
|
|
02e4c65239 |
181
.claude/hooks/guard.py
Normal file
181
.claude/hooks/guard.py
Normal file
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Block tool calls that violate a written contributor rule.
|
||||
|
||||
Wired as a Claude Code `PreToolUse` hook in `.claude/settings.json`. Every rule
|
||||
here cites a rule already written down in `.github/CONTRIBUTING.md` or
|
||||
`AGENTS.md` — this file enforces those documents, it does not add policy of its
|
||||
own. A rule that is not written down there does not belong here.
|
||||
|
||||
Reads the hook event as JSON on stdin, writes a JSON decision on stdout, and
|
||||
exits 0 either way. No subprocess, no filesystem writes, no network.
|
||||
|
||||
Escape hatch: include `# policy-override: <reason>` in a Bash command to state
|
||||
an exception explicitly rather than working around the guard silently.
|
||||
|
||||
Known limitation: rules match the command text, so a command that passes a
|
||||
protected path to another program as a quoted argument — writing a commit
|
||||
message or a PR comment about `docs/images`, for example — is denied even
|
||||
though it changes nothing. Heredoc bodies are stripped before matching because
|
||||
they are unambiguously data, but quoted arguments are not: `rm -rf "docs/..."`
|
||||
is a real command, so stripping quotes would open a bypass. Use the override
|
||||
marker in that case.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
OVERRIDE_MARKER = "# policy-override:"
|
||||
|
||||
#: (pattern, reason). Patterns match the raw Bash command string.
|
||||
BASH_RULES: tuple[tuple[str, str], ...] = (
|
||||
(
|
||||
# Version-suffixed interpreters (pip3.12, python3.12 -m pip) bypass a bare
|
||||
# pip3? match. `uv pip` is unaffected: the anchor requires command position.
|
||||
r"(?:^|[;&|(\n])\s*(?:sudo\s+)?"
|
||||
r"(?:pip(?:[23](?:\.\d+)?)?|python(?:[23](?:\.\d+)?)?\s+-m\s+pip)\s+"
|
||||
r"(?:install|uninstall)\b",
|
||||
"CONTRIBUTING.md (Dependency Management): do not use pip directly. "
|
||||
"Use `uv add --package <pkg> <dep>`, `uv add --dev <dep>`, or `uv sync`.",
|
||||
),
|
||||
(
|
||||
# `-n` is only the skip-hooks flag for commit; on push it means --dry-run,
|
||||
# which is safe and must stay allowed.
|
||||
r"\bgit\b[^\n;&|]*\b(?:commit|push)\b[^\n;&|]*--no-verify\b"
|
||||
r"|\bgit\b[^\n;&|]*\bcommit\b[^\n;&|]*\s-n(?=\s|$)",
|
||||
"CONTRIBUTING.md (Commits): do not use --no-verify to skip hooks. "
|
||||
"Fix what pre-commit reports instead.",
|
||||
),
|
||||
(
|
||||
# Stops at shell separators so a later segment merely naming the path does
|
||||
# not trigger, and matches the directory with or without a trailing slash.
|
||||
r"\b(?:rm|mv)\b[^\n;&|]*\bdocs/images(?![\w.-])",
|
||||
"AGENTS.md (Changing Docs, rule 3): do not delete or rename files under "
|
||||
"docs/images/ — frozen doc snapshots still reference them.",
|
||||
),
|
||||
(
|
||||
# Write verbs and output redirection. Read-only access (cat, grep, less)
|
||||
# is deliberately allowed. `cp` is matched in either direction: failing
|
||||
# closed on a copy out of the tree is cheaper than missing a copy into it.
|
||||
r"\b(?:rm|mv|cp|tee|sed\s+-i)\b[^\n;&|]*\bdocs/v[0-9]"
|
||||
r"|>>?\s*[^\n;&|]*\bdocs/v[0-9]",
|
||||
"AGENTS.md (Changing Docs, rule 2): docs/v*/ are frozen release snapshots "
|
||||
"managed by devtools. Edit docs/edge/en/ instead.",
|
||||
),
|
||||
)
|
||||
|
||||
FROZEN_DOCS_REASON = (
|
||||
"AGENTS.md (Changing Docs, rule 2): docs/v*/ are frozen release snapshots "
|
||||
"managed by devtools. Edit the MDX under docs/edge/en/ instead, then sync the "
|
||||
"ar, ko, and pt-BR translations."
|
||||
)
|
||||
|
||||
|
||||
def deny(reason: str) -> None:
|
||||
"""Emit a deny decision.
|
||||
|
||||
The T201 suppression is deliberate and not a lint escape: stdout is the hook
|
||||
protocol itself — Claude Code parses this JSON to decide whether to proceed.
|
||||
"""
|
||||
print( # noqa: T201
|
||||
json.dumps(
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny",
|
||||
"permissionDecisionReason": reason,
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#: A heredoc opener, its same-line remainder, and the body up to the delimiter.
|
||||
HEREDOC = re.compile(
|
||||
r"(<<-?\s*[\"']?(\w+)[\"']?)([^\n]*)\n.*?^[ \t]*\2\b",
|
||||
re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
|
||||
#: Commands that execute their heredoc body instead of consuming it as data.
|
||||
SHELL_INTERPRETER = re.compile(r"\b(?:bash|sh|zsh|dash|ksh|eval)\b")
|
||||
|
||||
|
||||
def strip_heredocs(command: str) -> str:
|
||||
"""Replace data heredoc bodies with a placeholder.
|
||||
|
||||
A heredoc body is usually data — a commit message, a file being written, a
|
||||
PR body — not a command, and matching rules against it produces false
|
||||
denials, such as blocking a commit whose message merely discusses a
|
||||
protected path.
|
||||
|
||||
Two things keep that from becoming a bypass. A body piped into a shell
|
||||
(`bash <<'EOF'`) is executable, so it is left intact and still matched. And
|
||||
the opener's own line is preserved in full, so a redirection written after
|
||||
the delimiter (`cat <<'EOF' > docs/v1/x.mdx`) is still seen.
|
||||
"""
|
||||
|
||||
def replace(match: re.Match[str]) -> str:
|
||||
line_start = command.rfind("\n", 0, match.start()) + 1
|
||||
opener_line = command[line_start : match.start()] + match.group(1)
|
||||
if SHELL_INTERPRETER.search(opener_line):
|
||||
return match.group(0)
|
||||
return f"{match.group(1)}{match.group(3)}\nHEREDOC_BODY\n{match.group(2)}"
|
||||
|
||||
return HEREDOC.sub(replace, command)
|
||||
|
||||
|
||||
def bash_violation(command: str) -> str | None:
|
||||
"""Return the reason this command is blocked, or None if it is allowed."""
|
||||
if OVERRIDE_MARKER in command:
|
||||
return None
|
||||
inspectable = strip_heredocs(command)
|
||||
for pattern, reason in BASH_RULES:
|
||||
if re.search(pattern, inspectable):
|
||||
return reason
|
||||
return None
|
||||
|
||||
|
||||
def edits_frozen_docs(path: str) -> bool:
|
||||
"""True when path targets a frozen release snapshot under docs/v<digit>."""
|
||||
match = re.search(r"(?:^|/)docs/v[0-9]", path)
|
||||
return match is not None
|
||||
|
||||
|
||||
def target_path(tool_input: dict[str, Any]) -> str:
|
||||
"""The file a write-shaped tool is about to touch, or an empty string."""
|
||||
for key in ("file_path", "notebook_path"):
|
||||
value = tool_input.get(key)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return ""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
try:
|
||||
event = json.load(sys.stdin)
|
||||
except Exception:
|
||||
return
|
||||
if not isinstance(event, dict):
|
||||
return
|
||||
|
||||
tool_input = event.get("tool_input")
|
||||
if not isinstance(tool_input, dict):
|
||||
return
|
||||
|
||||
command = tool_input.get("command")
|
||||
if isinstance(command, str):
|
||||
reason = bash_violation(command)
|
||||
if reason is not None:
|
||||
deny(reason)
|
||||
return
|
||||
|
||||
if edits_frozen_docs(target_path(tool_input)):
|
||||
deny(FROZEN_DOCS_REASON)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
261
.claude/hooks/test_guard.py
Normal file
261
.claude/hooks/test_guard.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""Behavior tests for the contributor-rule guard hook.
|
||||
|
||||
Run with: uv run pytest .claude/hooks/test_guard.py -q
|
||||
|
||||
The module is loaded by path rather than imported by name: `.claude/hooks` is not
|
||||
a package and must not be added to `sys.path`, which would leak into other tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _load_guard() -> ModuleType:
|
||||
path = Path(__file__).parent / "guard.py"
|
||||
spec = importlib.util.spec_from_file_location("_guard_under_test", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
guard = _load_guard()
|
||||
|
||||
|
||||
BLOCKED_COMMANDS: list[tuple[str, str]] = [
|
||||
("pip install ruff", "do not use pip directly"),
|
||||
("pip3 install ruff", "do not use pip directly"),
|
||||
("sudo pip install ruff", "do not use pip directly"),
|
||||
("python -m pip install ruff", "do not use pip directly"),
|
||||
("uv sync && pip install ruff", "do not use pip directly"),
|
||||
("pip uninstall crewai", "do not use pip directly"),
|
||||
("git commit -m 'x' --no-verify", "do not use --no-verify"),
|
||||
("git commit -n -m 'x'", "do not use --no-verify"),
|
||||
("git push --no-verify", "do not use --no-verify"),
|
||||
("git -c user.name=x commit --no-verify -m 'x'", "do not use --no-verify"),
|
||||
("uv sync && git commit --no-verify -m 'x'", "do not use --no-verify"),
|
||||
("rm docs/images/flow.png", "docs/images/"),
|
||||
("mv docs/images/a.png docs/images/b.png", "docs/images/"),
|
||||
("rm -rf docs/v1.15.0", "frozen release snapshots"),
|
||||
# Version-suffixed interpreters must not bypass the pip rule.
|
||||
("pip3.12 install ruff", "do not use pip directly"),
|
||||
("pip2 install ruff", "do not use pip directly"),
|
||||
("python3.12 -m pip install ruff", "do not use pip directly"),
|
||||
# The protected directory named without a trailing slash.
|
||||
("rm -rf docs/images", "docs/images/"),
|
||||
("mv docs/images docs/img", "docs/images/"),
|
||||
# Writes into a frozen snapshot by redirection or copy, not just rm/mv.
|
||||
("echo x > docs/v1.15.0/index.mdx", "frozen release snapshots"),
|
||||
("cat tmp.mdx >> docs/v1.15.0/index.mdx", "frozen release snapshots"),
|
||||
("cp new.mdx docs/v1.15.0/index.mdx", "frozen release snapshots"),
|
||||
("sed -i 's/a/b/' docs/v1.15.0/index.mdx", "frozen release snapshots"),
|
||||
("tee docs/v1.15.0/index.mdx < new.mdx", "frozen release snapshots"),
|
||||
]
|
||||
|
||||
ALLOWED_COMMANDS: list[str] = [
|
||||
"uv add --package crewai httpx",
|
||||
"uv add --dev pytest",
|
||||
"uv sync --all-groups --all-extras",
|
||||
"uv pip install -e .",
|
||||
"uv run pytest lib/crewai/tests -x -q",
|
||||
"uv run pytest -n auto --dist=loadfile lib/crewai/tests",
|
||||
"git commit -m 'feat(agents): add skill loader'",
|
||||
"git push origin main",
|
||||
'grep -rn "pip install" .github/CONTRIBUTING.md',
|
||||
"echo 'never use --no-verify' >> notes.txt",
|
||||
'grep -rn "no-verify" .github/CONTRIBUTING.md',
|
||||
"uv run pytest -n auto lib/crewai/tests",
|
||||
"uv run pip-audit --skip-editable --ignore-vuln PYSEC-2024-277",
|
||||
"rm docs/edge/en/scratch.mdx",
|
||||
# Read-only access to a frozen snapshot is allowed; only writes are blocked.
|
||||
"cat docs/v1.15.0/index.mdx",
|
||||
"grep -rn 'agents' docs/v1.15.0/ > /tmp/hits.txt",
|
||||
"less docs/v1.15.0/index.mdx",
|
||||
"ls docs/images/",
|
||||
# `-n` on push is --dry-run, not skip-hooks.
|
||||
"git push -n origin main",
|
||||
"git push --dry-run origin main",
|
||||
# A protected path named in a later, unrelated command segment.
|
||||
"rm /tmp/scratch.txt && ls docs/images/",
|
||||
"rm /tmp/scratch.txt && cat docs/v1.15.0/index.mdx",
|
||||
# Paths that merely start with the same prefix.
|
||||
"rm docs/imagesets/old.png",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("command", "expected"), BLOCKED_COMMANDS)
|
||||
def test_blocked_commands_report_the_rule_they_violate(
|
||||
command: str, expected: str
|
||||
) -> None:
|
||||
reason = guard.bash_violation(command)
|
||||
assert reason is not None, f"expected {command!r} to be blocked"
|
||||
assert expected in reason
|
||||
|
||||
|
||||
@pytest.mark.parametrize("command", ALLOWED_COMMANDS)
|
||||
def test_allowed_commands_pass_through(command: str) -> None:
|
||||
assert guard.bash_violation(command) is None
|
||||
|
||||
|
||||
def test_blocked_reasons_cite_a_committed_document() -> None:
|
||||
for command, _ in BLOCKED_COMMANDS:
|
||||
reason = guard.bash_violation(command)
|
||||
assert reason is not None
|
||||
assert "CONTRIBUTING.md" in reason or "AGENTS.md" in reason
|
||||
|
||||
|
||||
HEREDOC_COMMIT = """git commit -m "$(cat <<'EOF'
|
||||
fix(agents): close guard bypasses found in review
|
||||
|
||||
- rm -rf docs/images without a trailing slash was not matched
|
||||
- pip install was reachable via pip3.12
|
||||
EOF
|
||||
)"
|
||||
"""
|
||||
|
||||
|
||||
def test_heredoc_bodies_are_data_not_commands() -> None:
|
||||
"""A commit message discussing a blocked command must not itself be blocked."""
|
||||
assert guard.bash_violation(HEREDOC_COMMIT) is None
|
||||
|
||||
|
||||
def test_a_heredoc_writing_into_a_frozen_snapshot_is_still_blocked() -> None:
|
||||
"""Stripping the body must not hide a redirection on the command line."""
|
||||
command = "cat > docs/v1.15.0/index.mdx <<'EOF'\nsome new content\nEOF\n"
|
||||
reason = guard.bash_violation(command)
|
||||
assert reason is not None
|
||||
assert "frozen release snapshots" in reason
|
||||
|
||||
|
||||
def test_a_redirect_after_the_heredoc_delimiter_is_still_seen() -> None:
|
||||
"""The opener's own line must survive stripping, redirect included."""
|
||||
command = "cat <<'EOF' > docs/v1.15.0/index.mdx\nsome new content\nEOF\n"
|
||||
reason = guard.bash_violation(command)
|
||||
assert reason is not None
|
||||
assert "frozen release snapshots" in reason
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("command", "expected"),
|
||||
[
|
||||
("bash <<'EOF'\nrm -rf docs/images/a.png\nEOF\n", "docs/images/"),
|
||||
("sh <<'EOF'\npip install ruff\nEOF\n", "do not use pip directly"),
|
||||
("bash -s <<'EOF'\ngit commit --no-verify -m x\nEOF\n", "--no-verify"),
|
||||
],
|
||||
)
|
||||
def test_a_heredoc_piped_into_a_shell_is_executable_not_data(
|
||||
command: str, expected: str
|
||||
) -> None:
|
||||
"""A body a shell will run must still be matched, not stripped as data."""
|
||||
reason = guard.bash_violation(command)
|
||||
assert reason is not None, f"expected {command!r} to be blocked"
|
||||
assert expected in reason
|
||||
|
||||
|
||||
def test_a_command_after_a_heredoc_is_still_inspected() -> None:
|
||||
command = "cat <<'EOF' > notes.txt\njust notes\nEOF\npip install ruff\n"
|
||||
reason = guard.bash_violation(command)
|
||||
assert reason is not None
|
||||
assert "do not use pip directly" in reason
|
||||
|
||||
|
||||
def test_override_marker_allows_a_stated_exception() -> None:
|
||||
command = "pip install vendored.whl # policy-override: offline wheel, no index"
|
||||
assert guard.bash_violation(command) is None
|
||||
|
||||
|
||||
def test_override_marker_does_not_leak_to_other_commands() -> None:
|
||||
assert guard.bash_violation("pip install ruff") is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"docs/v1.15.0/index.mdx",
|
||||
"/repo/docs/v1.15.0/concepts/agents.mdx",
|
||||
"/repo/docs/v2/index.mdx",
|
||||
],
|
||||
)
|
||||
def test_frozen_snapshot_paths_are_blocked(path: str) -> None:
|
||||
assert guard.edits_frozen_docs(path) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"docs/edge/en/index.mdx",
|
||||
"docs/edge/pt-BR/index.mdx",
|
||||
"lib/crewai/src/crewai/agent.py",
|
||||
"docs/versioning.md",
|
||||
"",
|
||||
],
|
||||
)
|
||||
def test_editable_paths_are_allowed(path: str) -> None:
|
||||
assert guard.edits_frozen_docs(path) is False
|
||||
|
||||
|
||||
def _run_main(event: Any, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any] | None:
|
||||
"""Drive main() with a hook event, returning the parsed decision if any."""
|
||||
payload = event if isinstance(event, str) else json.dumps(event)
|
||||
monkeypatch.setattr(sys, "stdin", io.StringIO(payload))
|
||||
out = io.StringIO()
|
||||
monkeypatch.setattr(sys, "stdout", out)
|
||||
guard.main()
|
||||
written = out.getvalue().strip()
|
||||
if not written:
|
||||
return None
|
||||
decision: dict[str, Any] = json.loads(written)
|
||||
return decision
|
||||
|
||||
|
||||
def test_main_denies_a_violating_bash_call(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
decision = _run_main({"tool_input": {"command": "pip install ruff"}}, monkeypatch)
|
||||
assert decision is not None
|
||||
output = decision["hookSpecificOutput"]
|
||||
assert output["hookEventName"] == "PreToolUse"
|
||||
assert output["permissionDecision"] == "deny"
|
||||
assert "do not use pip directly" in output["permissionDecisionReason"]
|
||||
|
||||
|
||||
def test_main_denies_a_write_to_a_frozen_snapshot(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
decision = _run_main(
|
||||
{"tool_input": {"file_path": "/repo/docs/v1.15.0/index.mdx"}}, monkeypatch
|
||||
)
|
||||
assert decision is not None
|
||||
reason = decision["hookSpecificOutput"]["permissionDecisionReason"]
|
||||
assert "frozen release snapshots" in reason
|
||||
|
||||
|
||||
def test_main_stays_silent_on_an_allowed_call(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
assert _run_main({"tool_input": {"command": "uv sync"}}, monkeypatch) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"event",
|
||||
["", "not json", "[]", '"a string"', "{}", '{"tool_input": null}'],
|
||||
)
|
||||
def test_malformed_events_never_block(
|
||||
event: str, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
assert _run_main(event, monkeypatch) is None
|
||||
|
||||
|
||||
def test_a_bash_call_is_not_evaluated_as_a_file_path(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""A command mentioning docs/v* is judged by the Bash rules, not the path rule."""
|
||||
assert (
|
||||
_run_main({"tool_input": {"command": "cat docs/v1.15.0/x.mdx"}}, monkeypatch)
|
||||
is None
|
||||
)
|
||||
24
.claude/settings.json
Normal file
24
.claude/settings.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard.py\""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit|NotebookEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard.py\""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -26,7 +26,16 @@ plan.md
|
||||
conceptual_plan.md
|
||||
build_image
|
||||
chromadb-*.lock
|
||||
.claude
|
||||
# Everything under .claude is personal except the shared agent config below.
|
||||
# Git cannot re-include a file whose parent directory is excluded, so this
|
||||
# excludes the contents rather than the directory itself.
|
||||
.claude/*
|
||||
!.claude/settings.json
|
||||
!.claude/hooks/
|
||||
.claude/hooks/*
|
||||
!.claude/hooks/guard.py
|
||||
!.claude/hooks/test_guard.py
|
||||
CLAUDE.local.md
|
||||
.crewai/memory
|
||||
blogs/*
|
||||
secrets/*
|
||||
|
||||
@@ -61,6 +61,18 @@ repos:
|
||||
language: system
|
||||
pass_filenames: false
|
||||
stages: [pre-push, manual]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: agent-guard-hook
|
||||
name: agent-guard-hook
|
||||
# --confcutdir keeps the root conftest out: it assumes every test lives
|
||||
# under lib/<package>/tests/, which these do not.
|
||||
entry: >-
|
||||
bash -c 'source .venv/bin/activate && uv run pytest
|
||||
.claude/hooks/test_guard.py -q --confcutdir=.claude/hooks'
|
||||
language: system
|
||||
pass_filenames: false
|
||||
files: ^\.claude/hooks/
|
||||
- repo: https://github.com/commitizen-tools/commitizen
|
||||
rev: v4.10.1
|
||||
hooks:
|
||||
|
||||
11
CLAUDE.md
Normal file
11
CLAUDE.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# CrewAI
|
||||
|
||||
Contributor instructions for this repo live in `AGENTS.md`, shared by every coding
|
||||
agent. Claude Code does not discover that file on its own, so this import is what
|
||||
loads it — keep the two in sync by editing `AGENTS.md`, never by duplicating rules
|
||||
here.
|
||||
|
||||
@AGENTS.md
|
||||
|
||||
Personal, uncommitted preferences belong in `CLAUDE.local.md`, which Claude Code loads
|
||||
alongside this file and which `.gitignore` keeps out of the repo.
|
||||
@@ -115,6 +115,7 @@ ignore-decorators = ["typing.overload"]
|
||||
"lib/cli/tests/**/*.py" = ["S101", "RET504", "S105", "S106"] # Allow assert statements in tests
|
||||
"lib/crewai-core/tests/**/*.py" = ["S101", "RET504", "S105", "S106"] # Allow assert statements in tests
|
||||
"lib/devtools/tests/**/*.py" = ["S101"]
|
||||
".claude/hooks/test_*.py" = ["S101"] # Allow assert statements in tests
|
||||
|
||||
|
||||
[tool.mypy]
|
||||
|
||||
Reference in New Issue
Block a user