Skip to content

Commit 40148bf

Browse files
mpecanclaude
andauthored
feat: extract shared hook types into tokf-hook-types crate (#314)
## Summary - Extract `ExternalEngineConfig`, `ErrorFallback`, `HookFormat`, `PermissionVerdict`, `RewriteConfig`, `PermissionsConfig`, and related types into a new `tokf-hook-types` crate - Enables external tools to share the config format without duplicating type definitions or risking format drift - Adds `Serialize` derives for round-trip TOML support, `skip_serializing_if` for clean output, and `Default` impl for `ExternalEngineConfig` - `tokf-cli` re-exports all types from the new crate for full backward compatibility ## Test plan - [x] `cargo fmt && cargo clippy --all-targets -- -D warnings` passes - [x] All 1359 tokf tests pass (0 failures) - [x] `tokf-hook-types` builds with only `serde` dependency - [x] Verify tokf-cli re-exports work correctly (no API breakage) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b7087f0 commit 40148bf

16 files changed

Lines changed: 243 additions & 172 deletions

File tree

.github/workflows/release.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ jobs:
4343
env:
4444
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
4545

46+
- name: Publish tokf-hook-types
47+
run: |
48+
LOCAL=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "tokf-hook-types") | .version')
49+
PUBLISHED=$(cargo search tokf-hook-types --limit 1 --color never | sed -n 's/^tokf-hook-types = "\([^"]*\)".*/\1/p')
50+
if [ "$LOCAL" = "$PUBLISHED" ]; then
51+
echo "::notice::tokf-hook-types@$LOCAL already published, skipping"
52+
else
53+
cargo publish -p tokf-hook-types
54+
fi
55+
env:
56+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
57+
4658
- name: Publish tokf-filter
4759
run: |
4860
LOCAL=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[] | select(.name == "tokf-filter") | .version')

.release-please-config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
],
1919
"packages": {
2020
"crates/tokf-common": {},
21+
"crates/tokf-hook-types": {},
2122
"crates/tokf-filter": {},
2223
"crates/tokf-cli": { "component": "tokf" },
2324
"crates/tokf-server": {},
@@ -34,7 +35,7 @@
3435
{
3536
"type": "linked-versions",
3637
"groupName": "workspace",
37-
"components": ["tokf-common", "tokf-filter", "tokf", "tokf-server", "catalog-types"]
38+
"components": ["tokf-common", "tokf-hook-types", "tokf-filter", "tokf", "tokf-server", "catalog-types"]
3839
}
3940
]
4041
}

.release-please-manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"crates/tokf-cli": "0.2.38",
55
"crates/tokf-server": "0.2.38",
66
"crates/tokf-server/generated": "0.2.38",
7+
"crates/tokf-hook-types": "0.2.38",
78
"crates/e2e-tests": "0.1.26"
89
}

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["crates/tokf-cli", "crates/tokf-common", "crates/tokf-filter", "crates/tokf-server", "crates/crdb-test-macro", "crates/e2e-tests"]
2+
members = ["crates/tokf-cli", "crates/tokf-common", "crates/tokf-filter", "crates/tokf-hook-types", "crates/tokf-server", "crates/crdb-test-macro", "crates/e2e-tests"]
33
resolver = "2"
44

55
[workspace.package]

crates/tokf-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ path = "src/main.rs"
1616
[dependencies]
1717
tokf-common = { path = "../tokf-common", version = "0.2.38", features = ["validation"] }
1818
tokf-filter = { path = "../tokf-filter", version = "0.2.38" }
19+
tokf-hook-types = { path = "../tokf-hook-types", version = "0.2.38" }
1920
clap = { version = "4", features = ["derive", "env"] }
2021
toml = "1.0"
2122
serde = { version = "1", features = ["derive"] }

crates/tokf-cli/src/hook/permission_engine.rs

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,69 +10,12 @@ use std::io::Write;
1010
use std::process::{Command, Stdio};
1111
use std::time::Duration;
1212

13-
use serde::Deserialize;
1413
use serde_json::Value;
1514

1615
use super::permissions::PermissionVerdict;
1716
use super::types::HookFormat;
1817

19-
/// Configuration for an external permission engine.
20-
#[derive(Debug, Clone, Deserialize)]
21-
pub struct ExternalEngineConfig {
22-
/// Path to the external engine binary (resolved via PATH if not absolute).
23-
pub command: String,
24-
25-
/// Arguments passed to the engine. Use `{format}` as a placeholder for the
26-
/// tool format (e.g. `["hook", "handle", "--mode", "{format}"]`).
27-
/// The placeholder is replaced with the resolved format string before spawning.
28-
#[serde(default)]
29-
pub args: Vec<String>,
30-
31-
/// Timeout in milliseconds. Default: 5000 (5 seconds).
32-
#[serde(default = "default_timeout")]
33-
pub timeout_ms: u64,
34-
35-
/// What to do when the engine fails (crash, timeout, bad output).
36-
#[serde(default)]
37-
pub on_error: ErrorFallback,
38-
39-
/// Override the default format strings used for `{format}` substitution.
40-
/// Keys are the default names (`claude-code`, `gemini`, `cursor`);
41-
/// values are the replacements the engine expects.
42-
///
43-
/// Example: `{ "claude-code" = "claude", "gemini" = "google" }`
44-
#[serde(default)]
45-
pub format_map: std::collections::HashMap<String, String>,
46-
}
47-
48-
impl ExternalEngineConfig {
49-
/// Resolve the format string for a given hook format,
50-
/// applying `format_map` overrides if present.
51-
pub fn resolve_format(&self, format: HookFormat) -> String {
52-
let default = format.as_str();
53-
self.format_map
54-
.get(default)
55-
.cloned()
56-
.unwrap_or_else(|| default.to_string())
57-
}
58-
}
59-
60-
/// Behaviour when the external engine fails.
61-
#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)]
62-
#[serde(rename_all = "lowercase")]
63-
pub enum ErrorFallback {
64-
/// Fail closed — prompt user for permission (default).
65-
#[default]
66-
Ask,
67-
/// Fail open — auto-allow.
68-
Allow,
69-
/// Fall back to built-in rule matching.
70-
Builtin,
71-
}
72-
73-
const fn default_timeout() -> u64 {
74-
5000
75-
}
18+
pub use tokf_hook_types::{ErrorFallback, ExternalEngineConfig};
7619

7720
/// Error from the external permission engine.
7821
#[derive(Debug)]

crates/tokf-cli/src/hook/permissions.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,7 @@
99
use serde_json::Value;
1010
use std::path::PathBuf;
1111

12-
/// Verdict from checking a command against Claude Code's permission rules.
13-
#[derive(Debug, PartialEq, Eq, Clone)]
14-
pub enum PermissionVerdict {
15-
/// No deny/ask rules matched — safe to auto-allow.
16-
Allow,
17-
/// A deny rule matched — pass through to Claude Code's native deny handling.
18-
Deny,
19-
/// An ask rule matched — rewrite the command but let the tool prompt the user.
20-
Ask,
21-
}
12+
pub use tokf_hook_types::PermissionVerdict;
2213

2314
/// Check `cmd` against Claude Code's deny/ask permission rules.
2415
///

crates/tokf-cli/src/hook/types.rs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,6 @@
11
use serde::{Deserialize, Serialize};
22

3-
/// Which hook format is being processed — determines response shape
4-
/// and which JSON field carries the permission decision.
5-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6-
pub enum HookFormat {
7-
/// Claude Code: `hookSpecificOutput.permissionDecision`
8-
ClaudeCode,
9-
/// Gemini CLI: `decision`
10-
Gemini,
11-
/// Cursor: `permission`
12-
Cursor,
13-
}
14-
15-
impl HookFormat {
16-
/// Default string identifier for this format.
17-
///
18-
/// Used as the default `{format}` template value in external engine args.
19-
pub const fn as_str(self) -> &'static str {
20-
match self {
21-
Self::ClaudeCode => "claude-code",
22-
Self::Gemini => "gemini",
23-
Self::Cursor => "cursor",
24-
}
25-
}
26-
}
3+
pub use tokf_hook_types::HookFormat;
274

285
/// Claude Code `PreToolUse` hook input (read from stdin).
296
#[derive(Debug, Clone, Deserialize)]

crates/tokf-cli/src/rewrite/types.rs

Lines changed: 3 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,6 @@
1-
use serde::Deserialize;
2-
3-
const fn default_true() -> bool {
4-
true
5-
}
6-
7-
/// User-provided overrides loaded from `rewrites.toml`.
8-
#[derive(Debug, Clone, Default, Deserialize)]
9-
pub struct RewriteConfig {
10-
/// Additional skip patterns (commands matching these are never rewritten).
11-
pub skip: Option<SkipConfig>,
12-
13-
/// Pipe stripping and prefer-less-context behaviour.
14-
pub pipe: Option<PipeConfig>,
15-
16-
/// User-defined rewrite rules (checked before auto-generated filter rules).
17-
#[serde(default)]
18-
pub rewrite: Vec<RewriteRule>,
19-
20-
/// Permission engine configuration (external sub-hook delegation).
21-
pub permissions: Option<PermissionsConfig>,
22-
}
23-
24-
/// Configuration for the permission decision engine.
25-
#[derive(Debug, Clone, Deserialize)]
26-
pub struct PermissionsConfig {
27-
/// Which engine to use: `"builtin"` (default) or `"external"`.
28-
#[serde(default)]
29-
pub engine: PermissionEngineType,
30-
31-
/// Configuration for the external engine (required when `engine = "external"`).
32-
pub external: Option<crate::hook::permission_engine::ExternalEngineConfig>,
33-
}
34-
35-
/// Which permission engine to use.
36-
#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)]
37-
#[serde(rename_all = "lowercase")]
38-
pub enum PermissionEngineType {
39-
/// Built-in deny/ask rule matching from Claude Code settings.json.
40-
#[default]
41-
Builtin,
42-
/// Delegate to an external process (sub-hook).
43-
External,
44-
}
45-
46-
/// Controls how tokf handles piped commands during rewriting.
47-
#[derive(Debug, Clone, Deserialize)]
48-
pub struct PipeConfig {
49-
/// Whether to strip simple pipes (tail/head/grep) when a filter matches.
50-
/// Default: true (current behaviour).
51-
#[serde(default = "default_true")]
52-
pub strip: bool,
53-
54-
/// When true and a pipe is stripped, inject `--prefer-less` so that at
55-
/// runtime tokf compares filtered vs piped output and uses whichever is
56-
/// smaller.
57-
#[serde(default)]
58-
pub prefer_less: bool,
59-
}
60-
61-
/// Extra skip patterns from user config.
62-
#[derive(Debug, Clone, Default, Deserialize)]
63-
pub struct SkipConfig {
64-
/// Regex patterns — if any matches the command, rewriting is skipped.
65-
#[serde(default)]
66-
pub patterns: Vec<String>,
67-
}
1+
pub use tokf_hook_types::{
2+
PermissionEngineType, PermissionsConfig, PipeConfig, RewriteConfig, RewriteRule, SkipConfig,
3+
};
684

695
/// Options that control how the rewrite system generates `tokf run` commands.
706
#[derive(Debug, Clone, Default)]
@@ -74,17 +10,6 @@ pub struct RewriteOptions {
7410
pub no_mask_exit_code: bool,
7511
}
7612

77-
/// A single rewrite rule: match a command and replace it.
78-
#[derive(Debug, Clone, Deserialize)]
79-
pub struct RewriteRule {
80-
/// Regex pattern to match against the command string.
81-
#[serde(rename = "match")]
82-
pub match_pattern: String,
83-
84-
/// Replacement template. Supports `{0}` (full match), `{1}`, `{2}`, etc.
85-
pub replace: String,
86-
}
87-
8813
#[cfg(test)]
8914
#[allow(clippy::unwrap_used, clippy::expect_used)]
9015
mod tests {

0 commit comments

Comments
 (0)