Skip to content

Commit e22cfb3

Browse files
committed
feat: add aifr hook check-command for Claude Code pre-tool-use hooks
Adds a new `aifr hook` parent command with a `check-command` sub-command designed for use as a Claude Code PreToolUse hook. When configured for the Bash tool matcher, it reads the hook payload from stdin, analyzes the shell command, and denies it with an aifr alternative suggestion when the operation can be safely handled by aifr. Recognized commands: cat, head, tail, grep/rg, find, ls, wc, stat, diff, sed -n, sha256sum/md5sum/sha1sum, hexdump/xxd, git log, git diff. Complex commands (pipes, chains, subshells) are passed through silently. The `hook` parent command is extensible for future hook integrations. https://claude.ai/code/session_017inmawi6PUgMy9zSu6EXKv
1 parent 54feea8 commit e22cfb3

File tree

10 files changed

+1394
-74
lines changed

10 files changed

+1394
-74
lines changed

cmd/aifr/cmd_hook.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2026 — see LICENSE file for terms.
2+
package main
3+
4+
import "github.com/spf13/cobra"
5+
6+
var hookCmd = &cobra.Command{
7+
Use: "hook",
8+
Short: "Hooks for AI coding agent integration",
9+
Long: `Commands designed for use as hooks in AI coding agents such as Claude Code.
10+
11+
These sub-commands read hook payloads from stdin and write hook responses
12+
to stdout, following the agent's hook protocol.
13+
14+
Example Claude Code configuration:
15+
16+
{
17+
"hooks": {
18+
"PreToolUse": [
19+
{
20+
"matcher": "Bash",
21+
"hooks": [
22+
{
23+
"type": "command",
24+
"command": "aifr hook check-command"
25+
}
26+
]
27+
}
28+
]
29+
}
30+
}`,
31+
}
32+
33+
func init() {
34+
rootCmd.AddCommand(hookCmd)
35+
}

cmd/aifr/cmd_hook_checkcommand.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2026 — see LICENSE file for terms.
2+
package main
3+
4+
import (
5+
"encoding/json"
6+
"io"
7+
"os"
8+
9+
"github.com/spf13/cobra"
10+
11+
"go.pennock.tech/aifr/internal/hookcmd"
12+
)
13+
14+
var checkCommandCmd = &cobra.Command{
15+
Use: "check-command",
16+
Short: "Suggest aifr alternatives for Bash tool calls",
17+
Long: `Reads a Claude Code PreToolUse hook payload from stdin, analyzes the
18+
shell command, and if aifr can handle it, outputs a hook response denying
19+
the Bash call and suggesting the aifr alternative.
20+
21+
If the command is not something aifr handles, exits silently (exit 0,
22+
no output) so the Bash call proceeds normally.
23+
24+
Recognized commands: cat, head, tail, grep/rg, find, ls, wc, stat,
25+
diff, sed -n, sha256sum/md5sum, hexdump/xxd, git log, git diff.
26+
27+
Usage in Claude Code settings:
28+
29+
{
30+
"hooks": {
31+
"PreToolUse": [
32+
{
33+
"matcher": "Bash",
34+
"hooks": [
35+
{
36+
"type": "command",
37+
"command": "aifr hook check-command"
38+
}
39+
]
40+
}
41+
]
42+
}
43+
}`,
44+
SilenceUsage: true,
45+
RunE: func(cmd *cobra.Command, args []string) error {
46+
input, err := io.ReadAll(os.Stdin)
47+
if err != nil {
48+
return err
49+
}
50+
51+
result, err := hookcmd.CheckCommand(input)
52+
if err != nil {
53+
return err
54+
}
55+
if result == nil {
56+
return nil
57+
}
58+
59+
enc := json.NewEncoder(os.Stdout)
60+
enc.SetIndent("", " ")
61+
return enc.Encode(result)
62+
},
63+
}
64+
65+
func init() {
66+
hookCmd.AddCommand(checkCommandCmd)
67+
}

go.mod

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,57 +13,31 @@ require (
1313
)
1414

1515
require (
16-
al.essio.dev/pkg/shellescape v1.5.1 // indirect
1716
dario.cat/mergo v1.0.2 // indirect
1817
github.com/Microsoft/go-winio v0.6.2 // indirect
1918
github.com/ProtonMail/go-crypto v1.4.1 // indirect
20-
github.com/charmbracelet/x/term v0.2.2 // indirect
2119
github.com/cloudflare/circl v1.6.3 // indirect
2220
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
23-
github.com/danieljoos/wincred v1.2.2 // indirect
2421
github.com/emirpasic/gods v1.18.1 // indirect
25-
github.com/fatih/color v1.18.0 // indirect
2622
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
2723
github.com/go-git/go-billy/v5 v5.8.0 // indirect
28-
github.com/goccy/go-yaml v1.19.2 // indirect
29-
github.com/godbus/dbus/v5 v5.1.0 // indirect
3024
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
31-
github.com/google/go-github/v80 v80.0.0 // indirect
32-
github.com/google/go-github/v82 v82.0.0 // indirect
33-
github.com/google/go-querystring v1.2.0 // indirect
3425
github.com/google/jsonschema-go v0.4.2 // indirect
35-
github.com/hashicorp/go-version v1.8.0 // indirect
3626
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3727
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
3828
github.com/kevinburke/ssh_config v1.6.0 // indirect
3929
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
40-
github.com/lmittmann/tint v1.1.2 // indirect
41-
github.com/mattn/go-colorable v0.1.14 // indirect
42-
github.com/mattn/go-isatty v0.0.20 // indirect
4330
github.com/pjbgf/sha1cd v0.5.0 // indirect
4431
github.com/segmentio/asm v1.2.1 // indirect
4532
github.com/segmentio/encoding v0.5.4 // indirect
4633
github.com/sergi/go-diff v1.4.0 // indirect
4734
github.com/skeema/knownhosts v1.3.2 // indirect
48-
github.com/spf13/afero v1.15.0 // indirect
4935
github.com/spf13/pflag v1.0.10 // indirect
50-
github.com/suzuki-shunsuke/ghtkn-go-sdk v0.2.2 // indirect
51-
github.com/suzuki-shunsuke/go-error-with-exit-code v1.0.0 // indirect
52-
github.com/suzuki-shunsuke/go-exec v0.0.1 // indirect
53-
github.com/suzuki-shunsuke/pinact/v3 v3.9.0 // indirect
54-
github.com/suzuki-shunsuke/slog-error v0.2.2 // indirect
55-
github.com/suzuki-shunsuke/slog-util v0.3.1 // indirect
56-
github.com/suzuki-shunsuke/urfave-cli-v3-util v0.2.0 // indirect
57-
github.com/urfave/cli/v3 v3.6.2 // indirect
5836
github.com/xanzy/ssh-agent v0.3.3 // indirect
5937
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
60-
github.com/zalando/go-keyring v0.2.6 // indirect
6138
golang.org/x/net v0.52.0 // indirect
6239
golang.org/x/oauth2 v0.36.0 // indirect
6340
golang.org/x/sys v0.42.0 // indirect
64-
golang.org/x/term v0.41.0 // indirect
65-
golang.org/x/text v0.35.0 // indirect
6641
golang.org/x/tools v0.42.0 // indirect
6742
gopkg.in/warnings.v0 v0.1.2 // indirect
68-
gopkg.in/yaml.v3 v3.0.1 // indirect
6943
)

go.sum

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
2-
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
31
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
42
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
53
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
@@ -13,24 +11,18 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
1311
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
1412
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
1513
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
16-
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
17-
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
1814
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
1915
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
2016
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
2117
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
2218
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
23-
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
24-
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
2519
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2620
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2721
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2822
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
2923
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
3024
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
3125
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
32-
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
33-
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
3426
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
3527
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
3628
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -43,27 +35,14 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj
4335
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
4436
github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104=
4537
github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
46-
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
47-
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
48-
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
49-
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
5038
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
5139
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
5240
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
5341
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
54-
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
5542
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
5643
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
57-
github.com/google/go-github/v80 v80.0.0 h1:BTyk3QOHekrk5VF+jIGz1TNEsmeoQG9K/UWaaP+EWQs=
58-
github.com/google/go-github/v80 v80.0.0/go.mod h1:pRo4AIMdHW83HNMGfNysgSAv0vmu+/pkY8nZO9FT9Yo=
59-
github.com/google/go-github/v82 v82.0.0 h1:OH09ESON2QwKCUVMYmMcVu1IFKFoaZHwqYaUtr/MVfk=
60-
github.com/google/go-github/v82 v82.0.0/go.mod h1:hQ6Xo0VKfL8RZ7z1hSfB4fvISg0QqHOqe9BP0qo+WvM=
61-
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
62-
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
6344
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
6445
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
65-
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
66-
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
6746
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
6847
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
6948
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@@ -79,12 +58,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
7958
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
8059
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
8160
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
82-
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
83-
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
84-
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
85-
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
86-
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
87-
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
8861
github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc=
8962
github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s=
9063
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
@@ -109,8 +82,6 @@ github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepq
10982
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
11083
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
11184
github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
112-
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
113-
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
11485
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
11586
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
11687
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -121,28 +92,10 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
12192
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
12293
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
12394
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
124-
github.com/suzuki-shunsuke/ghtkn-go-sdk v0.2.2 h1:rgGrzb4VDfGSFCXecxKbzJ2PxcJyplfKIu8wkyWGZ1U=
125-
github.com/suzuki-shunsuke/ghtkn-go-sdk v0.2.2/go.mod h1:RqXFhArJSKR/D+42ptl9pQFQ5ikIexxB7AxiFB1gOOo=
126-
github.com/suzuki-shunsuke/go-error-with-exit-code v1.0.0 h1:oVXrrYNGBq4POyITQNWKzwsYz7B2nUcqtDbeX4BfeEc=
127-
github.com/suzuki-shunsuke/go-error-with-exit-code v1.0.0/go.mod h1:kDFtLeftDiIUUHXGI3xq5eJ+uAOi50FPrxPENTHktJ0=
128-
github.com/suzuki-shunsuke/go-exec v0.0.1 h1:xn/lvYnRQOujUd46ph6f6IT0gVJIC8+3liSZKOjNj44=
129-
github.com/suzuki-shunsuke/go-exec v0.0.1/go.mod h1:KstSwIiQTKY34wEurUcFyKkaJDogBr5E3xxfdkkzvb0=
130-
github.com/suzuki-shunsuke/pinact/v3 v3.9.0 h1:9joYfYEfGSmQepuYZhl3akEgSr12MvU4O8JJR4WTMOw=
131-
github.com/suzuki-shunsuke/pinact/v3 v3.9.0/go.mod h1:v83U2W/fTfVB0kygS2jqoT7hBFY4OAK9TaAwe12QHbA=
132-
github.com/suzuki-shunsuke/slog-error v0.2.2 h1:z8rymlIlZcMA+ERnnhVigQ0Q+X0pxKqBfDzSIyGh6vU=
133-
github.com/suzuki-shunsuke/slog-error v0.2.2/go.mod h1:w45QyO2G0uiEuo9hhrcLqqRl3hmYon9jGgq9CrCxxOY=
134-
github.com/suzuki-shunsuke/slog-util v0.3.1 h1:tp4xgj4y/T2YcZkHtr7N2E49f5CiHl9o47/HKdwRQ4g=
135-
github.com/suzuki-shunsuke/slog-util v0.3.1/go.mod h1:PgZMd+2rC8pA9jBbXDfkI8mTuWYAiaVkKxjrbLtfN5I=
136-
github.com/suzuki-shunsuke/urfave-cli-v3-util v0.2.0 h1:ORT/qQxsKuWwuy2N/z2f2hmbKWmlS346/j4jGhxsxLo=
137-
github.com/suzuki-shunsuke/urfave-cli-v3-util v0.2.0/go.mod h1:BYtzUgA4oeUVUFoJIONWOquvIUy0cl7DpAeCya3mVJU=
138-
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
139-
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
14095
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
14196
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
14297
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
14398
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
144-
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
145-
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
14699
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
147100
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
148101
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
@@ -160,7 +113,6 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
160113
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
161114
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
162115
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
163-
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
164116
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
165117
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
166118
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

internal/hookcmd/hookcmd.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2026 — see LICENSE file for terms.
2+
package hookcmd
3+
4+
import "encoding/json"
5+
6+
// HookInput is the JSON payload received from a Claude Code hook on stdin.
7+
type HookInput struct {
8+
SessionID string `json:"session_id"`
9+
ToolName string `json:"tool_name"`
10+
ToolInput json.RawMessage `json:"tool_input"`
11+
HookEventName string `json:"hook_event_name"`
12+
}
13+
14+
// BashInput is the tool_input for a Bash tool call.
15+
type BashInput struct {
16+
Command string `json:"command"`
17+
}
18+
19+
// HookOutput is the JSON response for a Claude Code hook.
20+
type HookOutput struct {
21+
HookSpecificOutput *HookDecision `json:"hookSpecificOutput"`
22+
}
23+
24+
// HookDecision describes the hook's permission decision.
25+
type HookDecision struct {
26+
HookEventName string `json:"hookEventName"`
27+
Decision string `json:"permissionDecision"`
28+
Reason string `json:"permissionDecisionReason,omitempty"`
29+
}
30+
31+
// CheckCommand parses a PreToolUse hook payload and returns a hook output
32+
// denying the command with an aifr suggestion, or nil if no suggestion applies.
33+
func CheckCommand(input []byte) (*HookOutput, error) {
34+
var hi HookInput
35+
if err := json.Unmarshal(input, &hi); err != nil {
36+
return nil, err
37+
}
38+
39+
if hi.ToolName != "Bash" {
40+
return nil, nil
41+
}
42+
43+
var bi BashInput
44+
if err := json.Unmarshal(hi.ToolInput, &bi); err != nil {
45+
return nil, err
46+
}
47+
48+
suggestion := AnalyzeCommand(bi.Command)
49+
if suggestion == nil {
50+
return nil, nil
51+
}
52+
53+
return &HookOutput{
54+
HookSpecificOutput: &HookDecision{
55+
HookEventName: "PreToolUse",
56+
Decision: "deny",
57+
Reason: "This " + suggestion.Original +
58+
" invocation can be handled by aifr with access controls. Use: " +
59+
suggestion.AifrCommand,
60+
},
61+
}, nil
62+
}

0 commit comments

Comments
 (0)