agent-config-kit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. agent_config_kit-0.1.0/.gitignore +35 -0
  2. agent_config_kit-0.1.0/PKG-INFO +11 -0
  3. agent_config_kit-0.1.0/README.md +134 -0
  4. agent_config_kit-0.1.0/agent_config_kit/__init__.py +79 -0
  5. agent_config_kit-0.1.0/agent_config_kit/adapters/__init__.py +1 -0
  6. agent_config_kit-0.1.0/agent_config_kit/adapters/_wire/__init__.py +14 -0
  7. agent_config_kit-0.1.0/agent_config_kit/adapters/_wire/claude_settings.py +313 -0
  8. agent_config_kit-0.1.0/agent_config_kit/adapters/_wire/copilot_mcp.py +34 -0
  9. agent_config_kit-0.1.0/agent_config_kit/adapters/_wire/opencode_config.py +120 -0
  10. agent_config_kit-0.1.0/agent_config_kit/adapters/_wire/pi_mcp.py +36 -0
  11. agent_config_kit-0.1.0/agent_config_kit/adapters/claude.py +103 -0
  12. agent_config_kit-0.1.0/agent_config_kit/adapters/copilot.py +28 -0
  13. agent_config_kit-0.1.0/agent_config_kit/adapters/opencode.py +40 -0
  14. agent_config_kit-0.1.0/agent_config_kit/adapters/pi.py +28 -0
  15. agent_config_kit-0.1.0/agent_config_kit/cli.py +242 -0
  16. agent_config_kit-0.1.0/agent_config_kit/diff.py +138 -0
  17. agent_config_kit-0.1.0/agent_config_kit/installers.py +105 -0
  18. agent_config_kit-0.1.0/agent_config_kit/jsonio.py +45 -0
  19. agent_config_kit-0.1.0/agent_config_kit/manifest.py +152 -0
  20. agent_config_kit-0.1.0/agent_config_kit/models.py +234 -0
  21. agent_config_kit-0.1.0/agent_config_kit/paths.py +17 -0
  22. agent_config_kit-0.1.0/agent_config_kit/plan.py +181 -0
  23. agent_config_kit-0.1.0/agent_config_kit/prune.py +304 -0
  24. agent_config_kit-0.1.0/agent_config_kit/registry.py +123 -0
  25. agent_config_kit-0.1.0/pyproject.toml +32 -0
  26. agent_config_kit-0.1.0/tests/__init__.py +0 -0
  27. agent_config_kit-0.1.0/tests/test_cli.py +513 -0
  28. agent_config_kit-0.1.0/tests/test_diff.py +168 -0
  29. agent_config_kit-0.1.0/tests/test_installers.py +126 -0
  30. agent_config_kit-0.1.0/tests/test_jsonio.py +50 -0
  31. agent_config_kit-0.1.0/tests/test_manifest.py +271 -0
  32. agent_config_kit-0.1.0/tests/test_models.py +111 -0
  33. agent_config_kit-0.1.0/tests/test_paths.py +35 -0
  34. agent_config_kit-0.1.0/tests/test_plan.py +295 -0
  35. agent_config_kit-0.1.0/tests/test_prune.py +313 -0
  36. agent_config_kit-0.1.0/tests/test_registry.py +44 -0
  37. agent_config_kit-0.1.0/tests/test_wire_validation.py +78 -0
  38. agent_config_kit-0.1.0/uv.lock +327 -0
@@ -0,0 +1,35 @@
1
+ # Dependencies
2
+ node_modules/
3
+ .venv/
4
+ __pycache__/
5
+ *.pyc
6
+
7
+ # Environment / secrets
8
+ .env
9
+ .env.local
10
+ .env.*.local
11
+ *.secret
12
+
13
+ # Editor
14
+ .vscode/settings.json
15
+ .idea/
16
+ *.swp
17
+ *.swo
18
+
19
+ # OS
20
+ .DS_Store
21
+ Thumbs.db
22
+
23
+ # Build artifacts
24
+ dist/
25
+ build/
26
+ *.egg-info/
27
+ # omnigraph binary downloaded by hatch_build.py — not committed
28
+ _bin/
29
+ # omnigraph cluster state (populated by `omnigraph cluster import`)
30
+ __cluster/
31
+
32
+ # MCP server logs / caches
33
+ .mcp-cache/
34
+ mcp-server.log
35
+ .ruff_cache/
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-config-kit
3
+ Version: 0.1.0
4
+ Summary: Unified MCP server, skill, and hook registration across coding-agent platforms
5
+ License-Expression: BSD-3-Clause
6
+ Classifier: Programming Language :: Python :: 3
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: pydantic<3,>=2
9
+ Provides-Extra: cli
10
+ Requires-Dist: cyclopts<5,>=4; extra == 'cli'
11
+ Requires-Dist: rich>=13; extra == 'cli'
@@ -0,0 +1,134 @@
1
+ # agent-config-kit
2
+
3
+ Unified interface for registering MCP servers, skills, and hooks/extensions
4
+ across coding-agent platforms (Claude Code, Pi, GitHub Copilot, OpenCode,
5
+ Kilo Code, ...) without reimplementing every agent's config-file quirks.
6
+
7
+ Canonical, capability-cluster `pydantic` models (`StdioServer`/`RemoteServer`,
8
+ `DeclarativeHook`/`PluginRegistration`, `SkillSource`, ...) describe *what* to
9
+ register; a small per-platform adapter and a shared read-merge-write
10
+ orchestration layer (`apply`/`apply_all`) handle *where* and in what wire
11
+ format each platform expects it.
12
+
13
+ ```python
14
+ from agent_config_kit import RegistrationBundle, StdioServer, apply_all
15
+
16
+ bundle = RegistrationBundle(
17
+ mcp_servers={"my-tool": StdioServer(command="uvx", args=["my-tool", "serve"])},
18
+ )
19
+ apply_all(bundle)
20
+ ```
21
+
22
+ See `docs/design/agent-config-kit-spec.md` in this repo for the full design.
23
+
24
+ ## CLI (`ac-kit`)
25
+
26
+ A project without its own Python tooling (a plain Node repo, a
27
+ shell-scripted dotfiles setup, a CI job) can drive the same
28
+ `RegistrationBundle`/`apply` machinery declaratively via a TOML manifest and
29
+ the `ac-kit` console script, gated behind the `cli` extra:
30
+
31
+ ```bash
32
+ uv tool install 'agent-config-kit[cli]'
33
+ # or: pip install 'agent-config-kit[cli]'
34
+ ```
35
+
36
+ ### Manifest format
37
+
38
+ ```toml
39
+ # agent-config.toml
40
+
41
+ instructions = "See AGENTS.md" # optional — must come before any table/
42
+ # array-of-tables header, or TOML parses
43
+ # it as belonging to the preceding one
44
+
45
+ [options]
46
+ scope = "global" # "global" | "project" — default: "global"
47
+ platforms = ["claude", "pi"] # optional allow-list; default: every
48
+ # detected platform
49
+
50
+ [mcp_servers.witan]
51
+ kind = "stdio"
52
+ command = "uvx"
53
+ args = ["witan", "serve"]
54
+ env = { WITAN_AUTHOR = "team" }
55
+
56
+ [mcp_servers.hosted-tool]
57
+ kind = "remote"
58
+ url = "https://example.com/mcp"
59
+ transport = "streamable-http" # "sse" | "http" | "streamable-http"
60
+
61
+ [[hooks]]
62
+ kind = "declarative"
63
+ event = "user_prompt_submit" # see HookEvent for valid values
64
+ command = "witan inject-context"
65
+
66
+ [[hooks]]
67
+ kind = "plugin"
68
+ entry_path = "extensions/pi/witan.ts" # resolved relative to this file
69
+
70
+ [[skills]]
71
+ name = "witan-task"
72
+ skill_md_path = "skills/witan-task/SKILL.md" # resolved relative to this file
73
+ ```
74
+
75
+ Table/field names mirror the Python model field names exactly
76
+ (`kind`, `command`, `args`, `env`, `event`, `entry_path`, ...) — see
77
+ `docs/design/agent-config-kit-cli-spec.md` for the full schema and rationale.
78
+
79
+ Skills follow the [Agent Skills specification](https://agentskills.io/specification):
80
+ `skill_md_path` must point to a file literally named `SKILL.md`, and its
81
+ parent directory is installed wholesale — `scripts/`, `references/`,
82
+ `assets/`, or any other supporting files alongside it are copied too, not
83
+ just `SKILL.md` itself. `name` must match the spec's frontmatter `name`
84
+ constraints (1-64 characters, lowercase alphanumeric segments separated by
85
+ single hyphens, no leading/trailing/consecutive hyphens) since it becomes
86
+ the installed skill's directory name.
87
+
88
+ ### `ac-kit apply`
89
+
90
+ Applies a manifest's MCP servers, hooks, and skills to one or more platforms:
91
+
92
+ ```bash
93
+ ac-kit apply agent-config.toml
94
+ ac-kit apply agent-config.toml --platform claude --platform pi
95
+ ac-kit apply agent-config.toml --scope project --dry-run
96
+ ```
97
+
98
+ - `--platform NAME` (repeatable) overrides the manifest's
99
+ `[options.platforms]`; with neither given, every detected platform is
100
+ targeted.
101
+ - `--scope global|project` overrides the manifest's `[options].scope` for
102
+ this run.
103
+ - `--dry-run` reports what would be written/removed without touching disk.
104
+ - `--prune` also removes entries a *previous* `apply --prune` of this same
105
+ manifest wrote but that have since been dropped from it (e.g. a deleted
106
+ `[mcp_servers.*]` table, a removed skill or hook). This is opt-in and only
107
+ ever removes what it can prove it wrote itself, tracked in a state file
108
+ (`<manifest>.lock.json` by default, override with `--state-file PATH`) — a
109
+ manifest's first-ever `--prune` run removes nothing, since there's no
110
+ recorded state yet to diff against, and a hand-edited key that's absent
111
+ from both the previous and current manifest is never touched.
112
+
113
+ Exit codes: `0` success, `1` a platform's target couldn't be parsed as JSON,
114
+ `2` the manifest failed to load.
115
+
116
+ ### `ac-kit validate`
117
+
118
+ Reports drift between a manifest and each platform's on-disk config, without
119
+ writing anything — useful in CI to catch configuration that's fallen out of
120
+ sync:
121
+
122
+ ```bash
123
+ ac-kit validate agent-config.toml
124
+ ac-kit validate agent-config.toml --platform claude
125
+ ```
126
+
127
+ Missing MCP servers/hooks, mismatched MCP server values, and missing skill/
128
+ plugin-hook files are all reported as drift; a target that fails to parse as
129
+ JSON is reported separately (not as drift, since there's nothing to compare).
130
+ `validate` never writes — pair it with `apply --prune` to actually
131
+ reconcile.
132
+
133
+ Exit codes: `0` no drift, `1` drift (or an unreadable target) found, `2` the
134
+ manifest failed to load.
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ from .diff import Drift, diff
4
+ from .jsonio import load_json_object, write_json
5
+ from .manifest import Manifest, ManifestError, ManifestOptions, load_manifest
6
+ from .models import (
7
+ AgentPlatform,
8
+ ApprovalMode,
9
+ ApprovalPolicy,
10
+ CapabilityScope,
11
+ DeclarativeHook,
12
+ FrontmatterRule,
13
+ Hook,
14
+ HookEvent,
15
+ InstructionsConfig,
16
+ LspServer,
17
+ McpServer,
18
+ MergeStrategy,
19
+ PluginRegistration,
20
+ RemoteServer,
21
+ Scope,
22
+ ScopeTarget,
23
+ SearchStrategy,
24
+ SkillSource,
25
+ StdioServer,
26
+ )
27
+ from .plan import InstallResult, RegistrationBundle, apply, apply_all
28
+ from .prune import (
29
+ PlatformState,
30
+ apply_with_prune,
31
+ default_state_path,
32
+ hook_identity,
33
+ load_state,
34
+ write_state,
35
+ )
36
+ from .registry import detect_installed_platforms, get_platform, known_platforms
37
+
38
+ __all__ = [
39
+ "AgentPlatform",
40
+ "ApprovalMode",
41
+ "ApprovalPolicy",
42
+ "CapabilityScope",
43
+ "DeclarativeHook",
44
+ "Drift",
45
+ "FrontmatterRule",
46
+ "Hook",
47
+ "HookEvent",
48
+ "InstallResult",
49
+ "InstructionsConfig",
50
+ "LspServer",
51
+ "Manifest",
52
+ "ManifestError",
53
+ "ManifestOptions",
54
+ "McpServer",
55
+ "MergeStrategy",
56
+ "PlatformState",
57
+ "PluginRegistration",
58
+ "RegistrationBundle",
59
+ "RemoteServer",
60
+ "Scope",
61
+ "ScopeTarget",
62
+ "SearchStrategy",
63
+ "SkillSource",
64
+ "StdioServer",
65
+ "apply",
66
+ "apply_all",
67
+ "apply_with_prune",
68
+ "default_state_path",
69
+ "detect_installed_platforms",
70
+ "diff",
71
+ "get_platform",
72
+ "hook_identity",
73
+ "known_platforms",
74
+ "load_json_object",
75
+ "load_manifest",
76
+ "load_state",
77
+ "write_json",
78
+ "write_state",
79
+ ]
@@ -0,0 +1 @@
1
+ """Per-platform wire-format adapters — quirks live here, never in the canonical models."""
@@ -0,0 +1,14 @@
1
+ """Vendored, codegen'd wire-format models — adapter-internal, not public API.
2
+
3
+ Generated with ``datamodel-code-generator`` (per spec D6) from real published
4
+ JSON Schemas (Claude Code's ``settings.json`` hooks section, OpenCode's
5
+ ``config.json``) or, where no upstream schema exists (Pi, GitHub Copilot/VS
6
+ Code), a minimal hand-authored one. Regenerate on demand when upstream
7
+ schemas change — deliberately not enforced by CI, since roughly half the
8
+ v1 platforms have no live schema to diff against automatically.
9
+
10
+ Not imported by ``agent_config_kit``'s adapters at runtime; used only to
11
+ validate adapter output against real schema shape (see
12
+ ``tests/test_wire_validation.py``) and as a reference for future adapter
13
+ work.
14
+ """
@@ -0,0 +1,313 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: claude-hooks.schema.json
3
+
4
+ from __future__ import annotations
5
+
6
+ from enum import StrEnum
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, PositiveFloat, RootModel, constr
10
+
11
+
12
+ class Shell(StrEnum):
13
+ bash = "bash"
14
+ powershell = "powershell"
15
+
16
+
17
+ class HookCommand1(BaseModel):
18
+ model_config = ConfigDict(
19
+ extra="forbid",
20
+ )
21
+ type: Literal["command"] = Field(..., description="Hook type")
22
+ command: constr(min_length=1) = Field(..., description="Shell command to execute")
23
+ timeout: PositiveFloat | None = Field(
24
+ None, description="Optional timeout in seconds"
25
+ )
26
+ async_: bool | None = Field(
27
+ None,
28
+ alias="async",
29
+ description="Run this hook asynchronously without blocking Claude Code",
30
+ )
31
+ asyncRewake: bool | None = Field(
32
+ None,
33
+ description="When true, the hook runs in the background and wakes the model when it exits with code 2. Implies async.",
34
+ )
35
+ shell: Shell | None = Field(
36
+ None,
37
+ description='Shell interpreter for the command. "bash" uses the login shell (bash/zsh/sh); "powershell" uses pwsh. Defaults to bash.',
38
+ )
39
+ if_: str | None = Field(
40
+ None,
41
+ alias="if",
42
+ description='Optional permission-rule-syntax filter (e.g., "Bash(git *)"). Evaluated only on tool-related events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied); on other events a hook with `if` set never runs. See https://code.claude.com/docs/en/hooks-guide#filter-hooks-with-matchers',
43
+ )
44
+ statusMessage: str | None = Field(
45
+ None, description="Custom spinner message displayed while the hook runs"
46
+ )
47
+ args: list[str] | None = Field(
48
+ None,
49
+ description="Argument list for exec form. When present, spawns the command directly without shell interpretation — each element is passed as-is, so path placeholders never need quoting. See https://code.claude.com/docs/en/hooks#command-hook-fields",
50
+ )
51
+
52
+
53
+ class HookCommand2(BaseModel):
54
+ model_config = ConfigDict(
55
+ extra="forbid",
56
+ )
57
+ type: Literal["prompt"] = Field(..., description="Hook type")
58
+ prompt: constr(min_length=1) = Field(
59
+ ...,
60
+ description="Prompt to evaluate with LLM. Use $ARGUMENTS placeholder for hook input JSON.",
61
+ )
62
+ model: str | None = Field(
63
+ None, description="Model to use for evaluation. Defaults to a fast model"
64
+ )
65
+ timeout: PositiveFloat | None = Field(
66
+ None, description="Optional timeout in seconds (default: 30)"
67
+ )
68
+ if_: str | None = Field(
69
+ None,
70
+ alias="if",
71
+ description="Optional permission-rule-syntax filter. Evaluated only on tool-related events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied); on other events a hook with `if` set never runs. See https://code.claude.com/docs/en/hooks-guide#filter-hooks-with-matchers",
72
+ )
73
+ statusMessage: str | None = Field(
74
+ None, description="Custom spinner message displayed while the hook runs"
75
+ )
76
+ continueOnBlock: bool | None = Field(
77
+ False,
78
+ description='When the prompt returns ok: false, feed the reason back to Claude and continue the turn instead of stopping. Implemented as continue: true on the resulting decision: "block". See https://code.claude.com/docs/en/hooks#prompt-hook-configuration',
79
+ )
80
+
81
+
82
+ class HookCommand3(BaseModel):
83
+ model_config = ConfigDict(
84
+ extra="forbid",
85
+ )
86
+ type: Literal["agent"] = Field(..., description="Hook type")
87
+ prompt: constr(min_length=1) = Field(
88
+ ...,
89
+ description="Prompt describing what to verify. Use $ARGUMENTS placeholder for hook input JSON.",
90
+ )
91
+ model: str | None = Field(
92
+ None, description="Model to use for evaluation. Defaults to a fast model"
93
+ )
94
+ timeout: PositiveFloat | None = Field(
95
+ None, description="Optional timeout in seconds (default: 60)"
96
+ )
97
+ if_: str | None = Field(
98
+ None,
99
+ alias="if",
100
+ description="Optional permission-rule-syntax filter. Evaluated only on tool-related events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied); on other events a hook with `if` set never runs. See https://code.claude.com/docs/en/hooks-guide#filter-hooks-with-matchers",
101
+ )
102
+ statusMessage: str | None = Field(
103
+ None, description="Custom spinner message displayed while the hook runs"
104
+ )
105
+
106
+
107
+ class HookCommand4(BaseModel):
108
+ model_config = ConfigDict(
109
+ extra="forbid",
110
+ )
111
+ type: Literal["http"] = Field(..., description="Hook type")
112
+ url: constr(min_length=1) = Field(
113
+ ...,
114
+ description="URL to POST hook input JSON to. Endpoint must accept POST requests and return JSON.",
115
+ )
116
+ headers: dict[str, str] | None = Field(
117
+ None,
118
+ description="Custom HTTP headers (e.g., Authorization: Bearer token). Values support $VAR_NAME or ${VAR_NAME} interpolation.",
119
+ )
120
+ allowedEnvVars: list[constr(min_length=1)] | None = Field(
121
+ None,
122
+ description="List of environment variable names permitted for interpolation in headers. If not set, no env var interpolation is allowed.",
123
+ )
124
+ timeout: PositiveFloat | None = Field(
125
+ None, description="Optional timeout in seconds (default: 30)"
126
+ )
127
+ if_: str | None = Field(
128
+ None,
129
+ alias="if",
130
+ description="Optional permission-rule-syntax filter. Evaluated only on tool-related events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied); on other events a hook with `if` set never runs. See https://code.claude.com/docs/en/hooks-guide#filter-hooks-with-matchers",
131
+ )
132
+ statusMessage: str | None = Field(
133
+ None, description="Custom spinner message displayed while the hook runs"
134
+ )
135
+
136
+
137
+ class HookCommand5(BaseModel):
138
+ model_config = ConfigDict(
139
+ extra="forbid",
140
+ )
141
+ type: Literal["mcp_tool"] = Field(..., description="Hook type")
142
+ server: constr(min_length=1) = Field(
143
+ ..., description="Name of a configured MCP server (must already be connected)"
144
+ )
145
+ tool: constr(min_length=1) = Field(
146
+ ..., description="Name of the tool to call on that server"
147
+ )
148
+ input: dict[str, Any] | None = Field(
149
+ None,
150
+ description="Arguments passed to the tool. String values support ${path} substitution from hook JSON input (e.g., ${tool_input.file_path}, ${cwd})",
151
+ )
152
+ timeout: PositiveFloat | None = Field(
153
+ None, description="Optional timeout in seconds (default: 60)"
154
+ )
155
+ if_: str | None = Field(
156
+ None,
157
+ alias="if",
158
+ description="Optional permission-rule-syntax filter. Evaluated only on tool-related events (PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied); on other events a hook with `if` set never runs. See https://code.claude.com/docs/en/hooks-guide#filter-hooks-with-matchers",
159
+ )
160
+ statusMessage: str | None = Field(
161
+ None, description="Custom spinner message displayed while the hook runs"
162
+ )
163
+
164
+
165
+ class HookCommand(
166
+ RootModel[HookCommand1 | HookCommand2 | HookCommand3 | HookCommand4 | HookCommand5]
167
+ ):
168
+ root: HookCommand1 | HookCommand2 | HookCommand3 | HookCommand4 | HookCommand5
169
+
170
+
171
+ class HookMatcher(BaseModel):
172
+ model_config = ConfigDict(
173
+ extra="forbid",
174
+ )
175
+ matcher: str | None = Field(
176
+ None,
177
+ description="Optional pattern to match event contexts, case-sensitive. Behavior depends on event type. See https://code.claude.com/docs/en/hooks#matcher-patterns for event-specific details and examples",
178
+ )
179
+ hooks: list[HookCommand] = Field(..., description="Array of hooks to execute")
180
+
181
+
182
+ class Hooks(BaseModel):
183
+ model_config = ConfigDict(
184
+ extra="forbid",
185
+ )
186
+ PreToolUse: list[HookMatcher] | None = Field(
187
+ None, description="Hooks that run before tool calls"
188
+ )
189
+ PostToolUse: list[HookMatcher] | None = Field(
190
+ None, description="Hooks that run after tool completion"
191
+ )
192
+ PostToolUseFailure: list[HookMatcher] | None = Field(
193
+ None, description="Hooks that run after a tool fails"
194
+ )
195
+ PermissionRequest: list[HookMatcher] | None = Field(
196
+ None, description="Hooks that run when a permission dialog appears"
197
+ )
198
+ Notification: list[HookMatcher] | None = Field(
199
+ None, description="Hooks that trigger on notifications"
200
+ )
201
+ UserPromptSubmit: list[HookMatcher] | None = Field(
202
+ None, description="Hooks that run when a user submits a prompt"
203
+ )
204
+ Stop: list[HookMatcher] | None = Field(
205
+ None,
206
+ description="Hooks that run when agents finish responding. Does not run on user interrupt",
207
+ )
208
+ StopFailure: list[HookMatcher] | None = Field(
209
+ None,
210
+ description="Hooks that run when a turn ends due to an API error (e.g., rate_limit, authentication_failed, billing_error, invalid_request, server_error, max_output_tokens, unknown). Matcher can scope to specific error types. Hook output and exit code are ignored. See https://code.claude.com/docs/en/hooks",
211
+ )
212
+ SubagentStart: list[HookMatcher] | None = Field(
213
+ None, description="Hooks that run when a subagent is spawned"
214
+ )
215
+ SubagentStop: list[HookMatcher] | None = Field(
216
+ None, description="Hooks that run when subagents finish responding"
217
+ )
218
+ PreCompact: list[HookMatcher] | None = Field(
219
+ None, description="Hooks that run before the context is compacted"
220
+ )
221
+ PostCompact: list[HookMatcher] | None = Field(
222
+ None,
223
+ description="Hooks that run after the context is compacted. See https://code.claude.com/docs/en/hooks",
224
+ )
225
+ Elicitation: list[HookMatcher] | None = Field(
226
+ None,
227
+ description="Hooks that run when an MCP server requests user input during a tool call. See https://code.claude.com/docs/en/hooks",
228
+ )
229
+ ElicitationResult: list[HookMatcher] | None = Field(
230
+ None,
231
+ description="Hooks that run after a user responds to an MCP elicitation, before the response is sent back to the server. See https://code.claude.com/docs/en/hooks",
232
+ )
233
+ TeammateIdle: list[HookMatcher] | None = Field(
234
+ None,
235
+ description="Hooks that run when an agent team teammate is about to go idle. Exit code 2 sends feedback and keeps the teammate working. Does not support matchers. Agent teams are experimental. See https://code.claude.com/docs/en/hooks#teammateidle",
236
+ )
237
+ TaskCompleted: list[HookMatcher] | None = Field(
238
+ None,
239
+ description="Hooks that run when a task is being marked as completed. Exit code 2 prevents completion and sends feedback. Does not support matchers. See https://code.claude.com/docs/en/hooks#taskcompleted",
240
+ )
241
+ Setup: list[HookMatcher] | None = Field(
242
+ None,
243
+ description="UNDOCUMENTED. Hooks that run during repository initialization (--init, --init-only) or maintenance (--maintenance)",
244
+ )
245
+ InstructionsLoaded: list[HookMatcher] | None = Field(
246
+ None,
247
+ description="Hooks that run when a CLAUDE.md or .claude/rules/*.md file is loaded into context. Fires at session start and when files are lazily loaded (e.g., nested traversal, path glob match). No decision control; used for audit logging and observability. Does not support matchers. See https://code.claude.com/docs/en/hooks#instructionsloaded",
248
+ )
249
+ CwdChanged: list[HookMatcher] | None = Field(
250
+ None,
251
+ description="Hooks that run when the working directory changes. Provides cwd (new directory) and previous_cwd. Matchers are ignored; fires on every directory change. See https://code.claude.com/docs/en/hooks#cwdchanged",
252
+ )
253
+ FileChanged: list[HookMatcher] | None = Field(
254
+ None,
255
+ description="Hooks that run when a watched file is created, modified, or deleted. Supports filename matchers. Provides file_path and file_event_type (created, modified, deleted). See https://code.claude.com/docs/en/hooks#filechanged",
256
+ )
257
+ ConfigChange: list[HookMatcher] | None = Field(
258
+ None,
259
+ description="Hooks that run when settings, managed settings, or skill files change during a session. Supports matchers: user_settings, project_settings, local_settings, policy_settings, skills. Command handlers only. Exit code 2 blocks the change (except policy_settings which is audit-only). See https://code.claude.com/docs/en/hooks#configchange",
260
+ )
261
+ WorktreeCreate: list[HookMatcher] | None = Field(
262
+ None,
263
+ description='Hooks that run when a worktree is created via --worktree or isolation: "worktree" in subagents. Command handlers only, no matchers. Hook must print absolute path to created worktree on stdout; non-zero exit fails creation. See https://code.claude.com/docs/en/hooks#worktreecreate',
264
+ )
265
+ WorktreeRemove: list[HookMatcher] | None = Field(
266
+ None,
267
+ description="Hooks that run when a worktree is being removed at session exit or when a subagent finishes. Command handlers only, no matchers. Used for cleanup tasks; cannot block removal. See https://code.claude.com/docs/en/hooks#worktreeremove",
268
+ )
269
+ SessionStart: list[HookMatcher] | None = Field(
270
+ None, description="Hooks that run when a new session starts"
271
+ )
272
+ SessionEnd: list[HookMatcher] | None = Field(
273
+ None, description="Hooks that run when a session ends"
274
+ )
275
+ PostToolBatch: list[HookMatcher] | None = Field(
276
+ None,
277
+ description="Hooks that run after a full batch of parallel tool calls resolves, before the next model call. Exit code 2 blocks the agentic loop. Does not support matchers. See https://code.claude.com/docs/en/hooks",
278
+ )
279
+ TaskCreated: list[HookMatcher] | None = Field(
280
+ None,
281
+ description="Hooks that run when a task is being created via TaskCreate. Exit code 2 rolls back task creation. Does not support matchers. See https://code.claude.com/docs/en/hooks#taskcreated",
282
+ )
283
+ PermissionDenied: list[HookMatcher] | None = Field(
284
+ None,
285
+ description="Hooks that run when a tool call is denied by the auto mode classifier. Supports matchers on tool name. See https://code.claude.com/docs/en/hooks",
286
+ )
287
+ UserPromptExpansion: list[HookMatcher] | None = Field(
288
+ None,
289
+ description="Hooks that run when a user-typed command expands into a prompt, before it reaches Claude. Exit code 2 blocks the expansion. Supports matchers on command name. See https://code.claude.com/docs/en/hooks",
290
+ )
291
+
292
+
293
+ class ClaudeCodeHooksConfig(BaseModel):
294
+ hooks: Hooks | None = Field(
295
+ None,
296
+ description="Custom commands to run before/after tool executions. See https://code.claude.com/docs/en/hooks",
297
+ examples=[
298
+ {
299
+ "PostToolUse": [
300
+ {
301
+ "matcher": "Edit|Write",
302
+ "hooks": [
303
+ {
304
+ "type": "command",
305
+ "command": "prettier --write",
306
+ "timeout": 5,
307
+ }
308
+ ],
309
+ }
310
+ ]
311
+ }
312
+ ],
313
+ )
@@ -0,0 +1,34 @@
1
+ # generated by datamodel-codegen:
2
+ # filename: copilot-mcp.schema.json
3
+
4
+ from __future__ import annotations
5
+
6
+ from enum import StrEnum
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, ConfigDict
10
+
11
+
12
+ class Type(StrEnum):
13
+ stdio = "stdio"
14
+ sse = "sse"
15
+ http = "http"
16
+
17
+
18
+ class CopilotMcpServer(BaseModel):
19
+ model_config = ConfigDict(
20
+ extra="forbid",
21
+ )
22
+ type: Type
23
+ command: str | None = None
24
+ args: list[str] | None = None
25
+ url: str | None = None
26
+ inputs: list[dict[str, Any]] | None = None
27
+ env: dict[str, str] | None = None
28
+
29
+
30
+ class CopilotMcpConfig(BaseModel):
31
+ model_config = ConfigDict(
32
+ extra="forbid",
33
+ )
34
+ servers: dict[str, CopilotMcpServer] | None = None