Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions stripe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ def set_app_info(
}


from stripe._util import _emit_claude_code_hint

_emit_claude_code_hint()

# The beginning of the section generated from our OpenAPI spec
from importlib import import_module

Expand Down
15 changes: 15 additions & 0 deletions stripe/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,18 @@ def _wrapper(*args, **kwargs):
return class_method(*args, **kwargs)

return _wrapper


# If we detect environment variables that indicate we're running in a Claude Code
# session, emit a hint to the parent process so it can load the plugin.
# https://code.claude.com/docs/en/plugin-hints
def _claude_code_hint() -> str:
return '<claude-code-hint v="1" type="plugin" value="stripe@claude-plugins-official" />\n'


def _emit_claude_code_hint() -> None:
try:
if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_CODE_CHILD_SESSION"):
sys.stderr.write(_claude_code_hint())
except Exception:
pass
3 changes: 3 additions & 0 deletions tests/test_exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import subprocess
import sys

from stripe._util import _claude_code_hint


def assert_output(code: str, expected: str) -> None:
process = subprocess.Popen(
Expand All @@ -14,6 +16,7 @@ def assert_output(code: str, expected: str) -> None:

stdout, stderr = process.communicate()

stderr = stderr.replace(_claude_code_hint().encode(), b"")
assert not stderr, f"Error: {stderr.decode()}"

output = stdout.decode().strip()
Expand Down
34 changes: 34 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import io
import os
import sys
from collections import namedtuple

Expand All @@ -11,6 +13,8 @@
log_info,
log_debug,
sanitize_id,
_claude_code_hint,
_emit_claude_code_hint,
)
from stripe import Balance
from stripe._api_mode import ApiMode
Expand Down Expand Up @@ -178,3 +182,33 @@ def test_sanitize_id(self):
)
def test_get_api_mode(self, url: str, expected: ApiMode):
assert get_api_mode(url) == expected


class TestEmitClaudeCodeHint:
_HINT = _claude_code_hint()

def _capture(self, env_vars: dict) -> str:
buf = io.StringIO()
original = os.environ.copy()
try:
for k in ("CLAUDECODE", "CLAUDE_CODE_CHILD_SESSION"):
os.environ.pop(k, None)
os.environ.update(env_vars)
old_stderr, sys.stderr = sys.stderr, buf
try:
_emit_claude_code_hint()
finally:
sys.stderr = old_stderr
finally:
os.environ.clear()
os.environ.update(original)
return buf.getvalue()

def test_emits_when_CLAUDECODE_set(self):
assert self._capture({"CLAUDECODE": "1"}) == self._HINT

def test_emits_when_CLAUDE_CODE_CHILD_SESSION_set(self):
assert self._capture({"CLAUDE_CODE_CHILD_SESSION": "session-id"}) == self._HINT

def test_no_emit_without_env_vars(self):
assert self._capture({}) == ""