Skip to content
Open
4 changes: 4 additions & 0 deletions cmd/help/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/slackapi/slack-cli/internal/experiment"
"github.com/slackapi/slack-cli/internal/shared"
"github.com/slackapi/slack-cli/internal/style"
"github.com/slackapi/slack-cli/internal/useragent"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -75,6 +76,9 @@ func PrintHelpTemplate(cmd *cobra.Command, data style.TemplateData) {
if err != nil {
cmd.PrintErrln(err)
}

// Recommend the official Slack plugin when running inside Claude Code

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: Nice catch on the two call sites. This is likely a high-hit-rate call site and is subtle enough for most people to miss.

useragent.EmitClaudeCodePluginHint(cmd.ErrOrStderr())
}

// ════════════════════════════════════════════════════════════════════════════════
Expand Down
18 changes: 15 additions & 3 deletions cmd/help/help_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ import (

func TestHelpFunc(t *testing.T) {
tests := map[string]struct {
exampleCommands []style.ExampleCommand
experiments []string
expectedOutput []string
exampleCommands []style.ExampleCommand
experiments []string
envVars map[string]string
expectedOutput []string
expectedErrorOutput []string
}{
"basic command information is included": {
expectedOutput: []string{
Expand Down Expand Up @@ -63,6 +65,10 @@ func TestHelpFunc(t *testing.T) {
"unknown (invalid)",
},
},
"the claude code plugin hint is emitted inside claude code": {
envVars: map[string]string{"CLAUDECODE": "1"},
expectedErrorOutput: []string{"claude-code-hint"},
},
}

for name, tc := range tests {
Expand All @@ -74,6 +80,9 @@ func TestHelpFunc(t *testing.T) {
// Restore original EnabledExperiments
experiment.EnabledExperiments = _EnabledExperiments
}()
for name, value := range tc.envVars {
t.Setenv(name, value)
}

ctx := slackcontext.MockContext(t.Context())
clientsMock := shared.NewClientsMock()
Expand Down Expand Up @@ -102,6 +111,9 @@ func TestHelpFunc(t *testing.T) {
for _, expectedString := range tc.expectedOutput {
assert.Contains(t, clientsMock.GetStdoutOutput(), expectedString)
}
for _, expectedString := range tc.expectedErrorOutput {
assert.Contains(t, clientsMock.GetStderrOutput(), expectedString)
}
})
}
}
3 changes: 3 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ func NewRootCommand(clients *shared.ClientFactory, updateNotification *update.Up
// Check for an CLI update in the background while the command runs
updateNotification = update.New(clients, version.Raw(), "SLACK_SKIP_UPDATE")
updateNotification.CheckForUpdateInBackground(ctx, false)

// Recommend the official Slack plugin when running inside Claude Code
useragent.EmitClaudeCodePluginHint(clients.IO.WriteErr())
return nil
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
Expand Down
37 changes: 37 additions & 0 deletions internal/useragent/hint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2022-2026 Salesforce, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package useragent

import (
"fmt"
"io"
"os"
)

// claudeCodePluginHint is the marker to recommend the official Slack plugin to
// users of Claude Code.
//
// https://code.claude.com/docs/en/plugin-hints
const claudeCodePluginHint = `<claude-code-hint v="1" type="plugin" value="slack@claude-plugins-official" />`

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ note: I can't seem to have claude prompt but this matches spec!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you double check that the plugin was not already installed on your system?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing the marker prompt either. I spent some time digging into this and I have a lead:

CLAUDE_CODE_USE_BEDROCK=1 disables plugin hints entirely. So, our Slack work laptops will always disable this suggested prompt.

I'll test on my personal computer and share the results.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mwbrooks Fantastic intuition! 🧠 ✨ It makes me eager to test with a different build as well!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: I agree that internal/useragent reads well. If we get a second agent-conditioned behaviour, then we can consider a new pacakge.


// EmitClaudeCodePluginHint writes the Claude Code plugin recommendation marker
// to a writer that must be stderr to prompt installation without an appearance
// in actual outputs.
func EmitClaudeCodePluginHint(w io.Writer) {
if os.Getenv("CLAUDECODE") == "" {
return
}
fmt.Fprintln(w, claudeCodePluginHint)
}
53 changes: 53 additions & 0 deletions internal/useragent/hint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2022-2026 Salesforce, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package useragent

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
)

func Test_EmitClaudeCodePluginHint(t *testing.T) {
tests := map[string]struct {
claudeCode string
expected string
}{
"emits the hint on its own line inside claude code": {
claudeCode: "1",
expected: claudeCodePluginHint + "\n",
},
"emits the hint for any non-empty CLAUDECODE value": {
claudeCode: "true",
expected: claudeCodePluginHint + "\n",
},
"emits nothing when CLAUDECODE is unset": {
claudeCode: "",
expected: "",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
clearEnvVars(t)
t.Setenv("CLAUDECODE", tc.claudeCode)

var buf bytes.Buffer
EmitClaudeCodePluginHint(&buf)

assert.Equal(t, tc.expected, buf.String())
})
}
}
Loading