Skip to content
This repository was archived by the owner on Dec 17, 2025. It is now read-only.

Commit 56f7395

Browse files
committed
Install skills on new claude sessions
- Installed a global hook on init or install. - New --hook-mode for installation to return json
1 parent ae3a6ec commit 56f7395

File tree

9 files changed

+451
-93
lines changed

9 files changed

+451
-93
lines changed

go.mod

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/BurntSushi/toml v1.5.0
77
github.com/Masterminds/semver/v3 v3.4.0
88
github.com/creativeprojects/go-selfupdate v1.5.1
9+
github.com/gen2brain/beeep v0.11.1
910
github.com/gofrs/flock v0.13.0
1011
github.com/google/uuid v1.6.0
1112
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
@@ -16,25 +17,34 @@ require (
1617

1718
require (
1819
code.gitea.io/sdk/gitea v0.22.0 // indirect
20+
git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect
1921
github.com/42wim/httpsig v1.2.3 // indirect
2022
github.com/davidmz/go-pageant v1.0.2 // indirect
23+
github.com/esiqveland/notify v0.13.3 // indirect
2124
github.com/go-fed/httpsig v1.1.0 // indirect
25+
github.com/go-ole/go-ole v1.3.0 // indirect
26+
github.com/godbus/dbus/v5 v5.2.0 // indirect
2227
github.com/google/go-github/v30 v30.1.0 // indirect
2328
github.com/google/go-querystring v1.1.0 // indirect
2429
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
2530
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
2631
github.com/hashicorp/go-version v1.7.0 // indirect
2732
github.com/inconshreveable/mousetrap v1.1.0 // indirect
33+
github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
2834
github.com/kr/text v0.2.0 // indirect
2935
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
36+
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
3037
github.com/rivo/uniseg v0.4.7 // indirect
3138
github.com/rogpeppe/go-internal v1.14.1 // indirect
39+
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
40+
github.com/sergeymakinen/go-ico v1.0.0 // indirect
3241
github.com/spf13/pflag v1.0.10 // indirect
42+
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
3343
github.com/ulikunitz/xz v0.5.14 // indirect
3444
github.com/xanzy/go-gitlab v0.115.0 // indirect
3545
golang.org/x/crypto v0.41.0 // indirect
3646
golang.org/x/oauth2 v0.30.0 // indirect
37-
golang.org/x/sys v0.38.0 // indirect
47+
golang.org/x/sys v0.39.0 // indirect
3848
golang.org/x/term v0.37.0 // indirect
3949
golang.org/x/time v0.12.0 // indirect
4050
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
22
code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
3+
git.sr.ht/~jackmordaunt/go-toast v1.1.2 h1:/yrfI55LRt1M7H1vkaw+NaH1+L1CDxrqDltwm5euVuE=
4+
git.sr.ht/~jackmordaunt/go-toast v1.1.2/go.mod h1:jA4OqHKTQ4AFBdwrSnwnskUIIS3HYzlJSgdzCKqfavo=
35
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
46
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
57
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
@@ -12,14 +14,24 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
1214
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
1315
github.com/creativeprojects/go-selfupdate v1.5.1 h1:fuyEGFFfqcC8SxDGolcEPYPLXGQ9Mcrc5uRyRG2Mqnk=
1416
github.com/creativeprojects/go-selfupdate v1.5.1/go.mod h1:2uY75rP8z/D/PBuDn6mlBnzu+ysEmwOJfcgF8np0JIM=
17+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1518
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
1619
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1720
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
1821
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
22+
github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o=
23+
github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE=
1924
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
2025
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
26+
github.com/gen2brain/beeep v0.11.1 h1:EbSIhrQZFDj1K2fzlMpAYlFOzV8YuNe721A58XcCTYI=
27+
github.com/gen2brain/beeep v0.11.1/go.mod h1:jQVvuwnLuwOcdctHn/uyh8horSBNJ8uGb9Cn2W4tvoc=
2128
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
2229
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
30+
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
31+
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
32+
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
33+
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
34+
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
2335
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
2436
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
2537
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -42,6 +54,8 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe
4254
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
4355
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
4456
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
57+
github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o=
58+
github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ=
4559
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
4660
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
4761
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -54,6 +68,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
5468
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
5569
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
5670
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
71+
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
72+
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
5773
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
5874
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
5975
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -65,13 +81,25 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
6581
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
6682
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
6783
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
84+
github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=
85+
github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
86+
github.com/sergeymakinen/go-ico v1.0.0 h1:uL3khgvKkY6WfAetA+RqsguClBuu7HpvBB/nq/Jvr80=
87+
github.com/sergeymakinen/go-ico v1.0.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
6888
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
6989
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
7090
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
7191
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
7292
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
93+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
94+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
95+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
96+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
97+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
98+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
7399
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
74100
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
101+
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
102+
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
75103
github.com/ulikunitz/xz v0.5.14 h1:uv/0Bq533iFdnMHZdRBTOlaNMdb1+ZxXIlHDZHIHcvg=
76104
github.com/ulikunitz/xz v0.5.14/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
77105
github.com/xanzy/go-gitlab v0.115.0 h1:6DmtItNcVe+At/liXSgfE/DZNZrGfalQmBRmOcJjOn8=
@@ -91,8 +119,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
91119
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
92120
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
93121
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
94-
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
95-
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
122+
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
123+
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
96124
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
97125
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
98126
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
@@ -108,5 +136,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
108136
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
109137
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
110138
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
139+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
111140
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
112141
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/claude/claude.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package claude
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/sleuth-io/skills/internal/logger"
10+
)
11+
12+
// Output is an interface for printing messages during hook installation
13+
type Output interface {
14+
Println(string)
15+
PrintfErr(string, ...interface{})
16+
}
17+
18+
// GetClaudeDir returns the Claude Code directory (~/.claude)
19+
func GetClaudeDir() (string, error) {
20+
home, err := os.UserHomeDir()
21+
if err != nil {
22+
return "", fmt.Errorf("failed to get home directory: %w", err)
23+
}
24+
return filepath.Join(home, ".claude"), nil
25+
}
26+
27+
// InstallHooks installs Claude Code hooks (usage tracking and auto-update)
28+
func InstallHooks(out Output) error {
29+
claudeDir, err := GetClaudeDir()
30+
if err != nil {
31+
return fmt.Errorf("failed to get Claude directory: %w", err)
32+
}
33+
34+
// Install usage reporting hook
35+
if err := installUsageReportingHook(claudeDir, out); err != nil {
36+
return fmt.Errorf("failed to install usage reporting hook: %w", err)
37+
}
38+
39+
// Install session start hook for auto-update
40+
if err := installSessionStartHook(claudeDir, out); err != nil {
41+
return fmt.Errorf("failed to install session start hook: %w", err)
42+
}
43+
44+
return nil
45+
}
46+
47+
// installSessionStartHook installs the SessionStart hook for auto-updating artifacts
48+
func installSessionStartHook(claudeDir string, out Output) error {
49+
settingsPath := filepath.Join(claudeDir, "settings.json")
50+
51+
// Read existing settings or create new
52+
var settings map[string]interface{}
53+
if data, err := os.ReadFile(settingsPath); err == nil {
54+
if err := json.Unmarshal(data, &settings); err != nil {
55+
return fmt.Errorf("failed to parse settings.json: %w", err)
56+
}
57+
} else {
58+
settings = make(map[string]interface{})
59+
}
60+
61+
// Get or create hooks section
62+
hooks, ok := settings["hooks"].(map[string]interface{})
63+
if !ok {
64+
hooks = make(map[string]interface{})
65+
settings["hooks"] = hooks
66+
}
67+
68+
// Get or create SessionStart array
69+
sessionStart, ok := hooks["SessionStart"].([]interface{})
70+
if !ok {
71+
sessionStart = []interface{}{}
72+
}
73+
74+
// Check if our hook already exists
75+
hookExists := false
76+
for _, item := range sessionStart {
77+
if hookMap, ok := item.(map[string]interface{}); ok {
78+
if hooksArray, ok := hookMap["hooks"].([]interface{}); ok {
79+
for _, h := range hooksArray {
80+
if hMap, ok := h.(map[string]interface{}); ok {
81+
if cmd, ok := hMap["command"].(string); ok && (cmd == "skills install --hook-mode" || cmd == "skills install" || cmd == "skills install --error-on-change") {
82+
hookExists = true
83+
break
84+
}
85+
}
86+
}
87+
}
88+
}
89+
if hookExists {
90+
break
91+
}
92+
}
93+
94+
// Add hook if it doesn't exist
95+
if !hookExists {
96+
newHook := map[string]interface{}{
97+
"hooks": []interface{}{
98+
map[string]interface{}{
99+
"type": "command",
100+
"command": "skills install --hook-mode",
101+
},
102+
},
103+
}
104+
sessionStart = append(sessionStart, newHook)
105+
hooks["SessionStart"] = sessionStart
106+
107+
// Write back to file
108+
data, err := json.MarshalIndent(settings, "", " ")
109+
if err != nil {
110+
return fmt.Errorf("failed to marshal settings: %w", err)
111+
}
112+
113+
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
114+
return fmt.Errorf("failed to write settings.json: %w", err)
115+
}
116+
117+
log := logger.Get()
118+
log.Info("hook installed", "hook", "SessionStart", "command", "skills install --hook-mode")
119+
out.Println("\n✓ Installed auto-update hook to ~/.claude/settings.json")
120+
}
121+
122+
return nil
123+
}
124+
125+
// installUsageReportingHook installs the PostToolUse hook for usage tracking
126+
func installUsageReportingHook(claudeDir string, out Output) error {
127+
settingsPath := filepath.Join(claudeDir, "settings.json")
128+
129+
// Read existing settings or create new
130+
var settings map[string]interface{}
131+
if data, err := os.ReadFile(settingsPath); err == nil {
132+
if err := json.Unmarshal(data, &settings); err != nil {
133+
return fmt.Errorf("failed to parse settings.json: %w", err)
134+
}
135+
} else {
136+
settings = make(map[string]interface{})
137+
}
138+
139+
// Get or create hooks section
140+
hooks, ok := settings["hooks"].(map[string]interface{})
141+
if !ok {
142+
hooks = make(map[string]interface{})
143+
settings["hooks"] = hooks
144+
}
145+
146+
// Get or create PostToolUse array
147+
postToolUse, ok := hooks["PostToolUse"].([]interface{})
148+
if !ok {
149+
postToolUse = []interface{}{}
150+
}
151+
152+
// Check if our hook already exists
153+
hookExists := false
154+
for _, item := range postToolUse {
155+
if hookMap, ok := item.(map[string]interface{}); ok {
156+
if hooksArray, ok := hookMap["hooks"].([]interface{}); ok {
157+
for _, h := range hooksArray {
158+
if hMap, ok := h.(map[string]interface{}); ok {
159+
if cmd, ok := hMap["command"].(string); ok && cmd == "skills report-usage" {
160+
hookExists = true
161+
break
162+
}
163+
}
164+
}
165+
}
166+
}
167+
if hookExists {
168+
break
169+
}
170+
}
171+
172+
// Add hook if it doesn't exist
173+
if !hookExists {
174+
newHook := map[string]interface{}{
175+
"matcher": "Skill|Task|SlashCommand|mcp__.*",
176+
"hooks": []interface{}{
177+
map[string]interface{}{
178+
"type": "command",
179+
"command": "skills report-usage",
180+
},
181+
},
182+
}
183+
postToolUse = append(postToolUse, newHook)
184+
hooks["PostToolUse"] = postToolUse
185+
186+
// Write back to file
187+
data, err := json.MarshalIndent(settings, "", " ")
188+
if err != nil {
189+
return fmt.Errorf("failed to marshal settings: %w", err)
190+
}
191+
192+
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
193+
return fmt.Errorf("failed to write settings.json: %w", err)
194+
}
195+
196+
log := logger.Get()
197+
log.Info("hook installed", "hook", "PostToolUse", "command", "skills report-usage")
198+
out.Println("\n✓ Installed usage reporting hook to ~/.claude/settings.json")
199+
}
200+
201+
return nil
202+
}

internal/commands/commands.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ func RunDefaultCommand(cmd *cobra.Command, args []string) error {
1414

1515
// Check if skill.lock exists in current directory
1616
if _, err := os.Stat(constants.SkillLockFile); err == nil {
17-
// Lock file exists, run install
18-
return runInstall(cmd, args)
17+
// Lock file exists, run install (not in hook mode)
18+
return runInstall(cmd, args, false)
1919
}
2020

2121
// No lock file, show help

internal/commands/hooks.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package commands
2+
3+
import (
4+
"github.com/sleuth-io/skills/internal/claude"
5+
)
6+
7+
// outputAdapter adapts outputHelper to claude.Output interface
8+
type outputAdapter struct {
9+
out *outputHelper
10+
}
11+
12+
func (a *outputAdapter) Println(msg string) {
13+
a.out.println(msg)
14+
}
15+
16+
func (a *outputAdapter) PrintfErr(format string, args ...interface{}) {
17+
a.out.printfErr(format, args...)
18+
}
19+
20+
// installClaudeCodeHooks installs all Claude Code hooks (usage tracking and auto-update)
21+
func installClaudeCodeHooks(claudeDir string, out *outputHelper) error {
22+
// Note: claudeDir parameter is ignored, we use claude.InstallHooks which gets it internally
23+
adapter := &outputAdapter{out: out}
24+
return claude.InstallHooks(adapter)
25+
}

0 commit comments

Comments
 (0)