From 69290670d2df0b84e32c4a9ebd72279d61c4ffe5 Mon Sep 17 00:00:00 2001 From: John O'Sullivan Date: Thu, 6 Aug 2026 15:29:25 -0400 Subject: [PATCH 1/6] setting up claude code plugin hints --- stripe/__init__.py | 9 +++++++++ tests/test_exports.py | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/stripe/__init__.py b/stripe/__init__.py index dd958d504..c18a0f56e 100644 --- a/stripe/__init__.py +++ b/stripe/__init__.py @@ -1,6 +1,7 @@ from typing_extensions import TYPE_CHECKING, Literal from typing import Optional import os +import sys import warnings # Stripe Python bindings @@ -10,6 +11,14 @@ # Greg Brockman # Andrew Metcalf +try: + if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_CODE_CHILD_SESSION"): + sys.stderr.write( + '\n' + ) +except Exception: + pass + # Configuration variables from stripe._api_version import _ApiVersion diff --git a/tests/test_exports.py b/tests/test_exports.py index 44f11846d..193972086 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -1,24 +1,31 @@ # pyright: strict # we specifically test various import patterns +import os import stripe import subprocess import sys +_HINT_LINE = b'\n' -def assert_output(code: str, expected: str) -> None: + +def _run_import(code: str, env: "dict[str, str] | None" = None) -> "tuple[bytes, bytes]": process = subprocess.Popen( - [sys.executable, "-c", f"import stripe; print({code})"], + [sys.executable, "-c", code], stdout=subprocess.PIPE, stderr=subprocess.PIPE, + env=env, ) - stdout, stderr = process.communicate() + return stdout, stderr - assert not stderr, f"Error: {stderr.decode()}" - output = stdout.decode().strip() - # assert the output - assert output == expected +def assert_output(code: str, expected: str) -> None: + stdout, stderr = _run_import(f"import stripe; print({code})") + # Strip the plugin hint from stderr so this helper works both inside and + # outside a Claude Code session. + stderr = stderr.replace(_HINT_LINE, b"") + assert not stderr, f"Error: {stderr.decode()}" + assert stdout.decode().strip() == expected def test_can_import_request_options() -> None: @@ -171,3 +178,24 @@ def test_can_import_nested_params_types() -> None: assert SessionCreateParamsLineItem is not None assert AccountSessionCreateParamsComponents is not None + + +def _env_without_claude() -> "dict[str, str]": + return {k: v for k, v in os.environ.items() if k not in ("CLAUDECODE", "CLAUDE_CODE_CHILD_SESSION")} + + +def test_claude_code_hint_emits_when_CLAUDECODE_set() -> None: + env = {**_env_without_claude(), "CLAUDECODE": "1"} + _, stderr = _run_import("import stripe", env=env) + assert _HINT_LINE in stderr + + +def test_claude_code_hint_emits_when_CLAUDE_CODE_CHILD_SESSION_set() -> None: + env = {**_env_without_claude(), "CLAUDE_CODE_CHILD_SESSION": "session-id"} + _, stderr = _run_import("import stripe", env=env) + assert _HINT_LINE in stderr + + +def test_claude_code_hint_not_emitted_without_env_vars() -> None: + _, stderr = _run_import("import stripe", env=_env_without_claude()) + assert _HINT_LINE not in stderr From 8b79408a80ab3d5517c749bc77090264c60ff0f4 Mon Sep 17 00:00:00 2001 From: John O'Sullivan Date: Thu, 6 Aug 2026 16:30:22 -0400 Subject: [PATCH 2/6] moved the hint code to utils, shouldn't be so prominent --- stripe/__init__.py | 13 ++++--------- stripe/_util.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/stripe/__init__.py b/stripe/__init__.py index c18a0f56e..6f2ad0969 100644 --- a/stripe/__init__.py +++ b/stripe/__init__.py @@ -1,7 +1,6 @@ from typing_extensions import TYPE_CHECKING, Literal from typing import Optional import os -import sys import warnings # Stripe Python bindings @@ -11,14 +10,6 @@ # Greg Brockman # Andrew Metcalf -try: - if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_CODE_CHILD_SESSION"): - sys.stderr.write( - '\n' - ) -except Exception: - pass - # Configuration variables from stripe._api_version import _ApiVersion @@ -116,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 diff --git a/stripe/_util.py b/stripe/_util.py index 5a17b3932..91efecfd1 100644 --- a/stripe/_util.py +++ b/stripe/_util.py @@ -389,3 +389,16 @@ 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 _emit_claude_code_hint() -> None: + try: + if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_CODE_CHILD_SESSION"): + sys.stderr.write( + '\n' + ) + except Exception: + pass From 868641346974d01fbb844f1a39ba288f49f098f6 Mon Sep 17 00:00:00 2001 From: John O'Sullivan Date: Thu, 6 Aug 2026 16:40:19 -0400 Subject: [PATCH 3/6] moved tests someplace more appropriate --- tests/test_exports.py | 35 +++-------------------------------- tests/test_util.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/tests/test_exports.py b/tests/test_exports.py index 193972086..6dfd8fbd9 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -1,6 +1,5 @@ # pyright: strict # we specifically test various import patterns -import os import stripe import subprocess import sys @@ -8,21 +7,14 @@ _HINT_LINE = b'\n' -def _run_import(code: str, env: "dict[str, str] | None" = None) -> "tuple[bytes, bytes]": +def assert_output(code: str, expected: str) -> None: process = subprocess.Popen( - [sys.executable, "-c", code], + [sys.executable, "-c", f"import stripe; print({code})"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, - env=env, ) stdout, stderr = process.communicate() - return stdout, stderr - - -def assert_output(code: str, expected: str) -> None: - stdout, stderr = _run_import(f"import stripe; print({code})") - # Strip the plugin hint from stderr so this helper works both inside and - # outside a Claude Code session. + # Strip the plugin hint so this helper works inside and outside a Claude Code session. stderr = stderr.replace(_HINT_LINE, b"") assert not stderr, f"Error: {stderr.decode()}" assert stdout.decode().strip() == expected @@ -178,24 +170,3 @@ def test_can_import_nested_params_types() -> None: assert SessionCreateParamsLineItem is not None assert AccountSessionCreateParamsComponents is not None - - -def _env_without_claude() -> "dict[str, str]": - return {k: v for k, v in os.environ.items() if k not in ("CLAUDECODE", "CLAUDE_CODE_CHILD_SESSION")} - - -def test_claude_code_hint_emits_when_CLAUDECODE_set() -> None: - env = {**_env_without_claude(), "CLAUDECODE": "1"} - _, stderr = _run_import("import stripe", env=env) - assert _HINT_LINE in stderr - - -def test_claude_code_hint_emits_when_CLAUDE_CODE_CHILD_SESSION_set() -> None: - env = {**_env_without_claude(), "CLAUDE_CODE_CHILD_SESSION": "session-id"} - _, stderr = _run_import("import stripe", env=env) - assert _HINT_LINE in stderr - - -def test_claude_code_hint_not_emitted_without_env_vars() -> None: - _, stderr = _run_import("import stripe", env=_env_without_claude()) - assert _HINT_LINE not in stderr diff --git a/tests/test_util.py b/tests/test_util.py index 1331dbe20..985821a51 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,3 +1,5 @@ +import io +import os import sys from collections import namedtuple @@ -11,6 +13,7 @@ log_info, log_debug, sanitize_id, + _emit_claude_code_hint, ) from stripe import Balance from stripe._api_mode import ApiMode @@ -178,3 +181,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 = '\n' + + 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({}) == "" From ee66588f54db2fb99ab20df9cb553dc135855265 Mon Sep 17 00:00:00 2001 From: John O'Sullivan Date: Thu, 6 Aug 2026 16:46:16 -0400 Subject: [PATCH 4/6] more gracefully isolating hint parse behavior --- stripe/_util.py | 8 +++++--- tests/test_exports.py | 12 ++++++++---- tests/test_util.py | 3 ++- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/stripe/_util.py b/stripe/_util.py index 91efecfd1..1a57b01fe 100644 --- a/stripe/_util.py +++ b/stripe/_util.py @@ -394,11 +394,13 @@ def _wrapper(*args, **kwargs): # 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 '\n' + + def _emit_claude_code_hint() -> None: try: if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_CODE_CHILD_SESSION"): - sys.stderr.write( - '\n' - ) + sys.stderr.write(_claude_code_hint()) except Exception: pass diff --git a/tests/test_exports.py b/tests/test_exports.py index 6dfd8fbd9..90eb7d8b0 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -4,7 +4,7 @@ import subprocess import sys -_HINT_LINE = b'\n' +from stripe._util import _claude_code_hint def assert_output(code: str, expected: str) -> None: @@ -13,11 +13,15 @@ def assert_output(code: str, expected: str) -> None: stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) + stdout, stderr = process.communicate() - # Strip the plugin hint so this helper works inside and outside a Claude Code session. - stderr = stderr.replace(_HINT_LINE, b"") + + stderr = stderr.replace(_claude_code_hint().encode(), b"") assert not stderr, f"Error: {stderr.decode()}" - assert stdout.decode().strip() == expected + + output = stdout.decode().strip() + # assert the output + assert output == expected def test_can_import_request_options() -> None: diff --git a/tests/test_util.py b/tests/test_util.py index 985821a51..773aed64b 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -13,6 +13,7 @@ log_info, log_debug, sanitize_id, + _claude_code_hint, _emit_claude_code_hint, ) from stripe import Balance @@ -184,7 +185,7 @@ def test_get_api_mode(self, url: str, expected: ApiMode): class TestEmitClaudeCodeHint: - _HINT = '\n' + _HINT = _claude_code_hint() def _capture(self, env_vars: dict) -> str: buf = io.StringIO() From 1f2440b8734ec862791336913140f162f87d954c Mon Sep 17 00:00:00 2001 From: John O'Sullivan Date: Thu, 6 Aug 2026 23:23:55 -0400 Subject: [PATCH 5/6] formatting issue --- stripe/_util.py | 4 +++- tests/test_util.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/stripe/_util.py b/stripe/_util.py index 1a57b01fe..526afd256 100644 --- a/stripe/_util.py +++ b/stripe/_util.py @@ -400,7 +400,9 @@ def _claude_code_hint() -> str: def _emit_claude_code_hint() -> None: try: - if os.environ.get("CLAUDECODE") or os.environ.get("CLAUDE_CODE_CHILD_SESSION"): + if os.environ.get("CLAUDECODE") or os.environ.get( + "CLAUDE_CODE_CHILD_SESSION" + ): sys.stderr.write(_claude_code_hint()) except Exception: pass diff --git a/tests/test_util.py b/tests/test_util.py index 773aed64b..ad354fb5a 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -208,7 +208,10 @@ 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 + assert ( + self._capture({"CLAUDE_CODE_CHILD_SESSION": "session-id"}) + == self._HINT + ) def test_no_emit_without_env_vars(self): assert self._capture({}) == "" From 113e3dbdcd726e7212b4d08ebe129f5505e17a39 Mon Sep 17 00:00:00 2001 From: John O'Sullivan Date: Thu, 6 Aug 2026 23:32:00 -0400 Subject: [PATCH 6/6] inaccurate private Committed-By-Agent: claude --- stripe/_util.py | 4 ++-- tests/test_exports.py | 4 ++-- tests/test_util.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/stripe/_util.py b/stripe/_util.py index 526afd256..386894080 100644 --- a/stripe/_util.py +++ b/stripe/_util.py @@ -394,7 +394,7 @@ def _wrapper(*args, **kwargs): # 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: +def claude_code_hint_line() -> str: return '\n' @@ -403,6 +403,6 @@ def _emit_claude_code_hint() -> None: if os.environ.get("CLAUDECODE") or os.environ.get( "CLAUDE_CODE_CHILD_SESSION" ): - sys.stderr.write(_claude_code_hint()) + sys.stderr.write(claude_code_hint_line()) except Exception: pass diff --git a/tests/test_exports.py b/tests/test_exports.py index 90eb7d8b0..3aff0a530 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -4,7 +4,7 @@ import subprocess import sys -from stripe._util import _claude_code_hint +from stripe._util import claude_code_hint_line def assert_output(code: str, expected: str) -> None: @@ -16,7 +16,7 @@ def assert_output(code: str, expected: str) -> None: stdout, stderr = process.communicate() - stderr = stderr.replace(_claude_code_hint().encode(), b"") + stderr = stderr.replace(claude_code_hint_line().encode(), b"") assert not stderr, f"Error: {stderr.decode()}" output = stdout.decode().strip() diff --git a/tests/test_util.py b/tests/test_util.py index ad354fb5a..a8dc01c80 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -13,7 +13,7 @@ log_info, log_debug, sanitize_id, - _claude_code_hint, + claude_code_hint_line, _emit_claude_code_hint, ) from stripe import Balance @@ -185,7 +185,7 @@ def test_get_api_mode(self, url: str, expected: ApiMode): class TestEmitClaudeCodeHint: - _HINT = _claude_code_hint() + _HINT = claude_code_hint_line() def _capture(self, env_vars: dict) -> str: buf = io.StringIO()