From d9a18d90e0ff19679b3385ad0631f86a9477766d Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 8 Jul 2026 21:46:17 +0100 Subject: [PATCH 1/9] Add AGENTS.md and CLAUDE.md --- AGENTS.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 51 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2fb8caa --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,50 @@ +# Agent Guidelines + +This file captures the repository conventions for `guzzlehttp/command`. The +organization-wide +[contributing guidelines](https://github.com/guzzle/guzzle/blob/8.0/docs/overview.md#contributing) +are required reading and apply here too. + +## Branches + +- `2.0` is the unreleased next major version and `1.5` is the current stable + branch. Bug fixes target the oldest maintained branch they apply to and are + merged up; changes are never cherry-picked down. +- Backwards compatibility on released branches is paramount. Breaking changes + are only acceptable on an unreleased major version branch. + +## Commits and pull requests + +- Write a single concise commit subject line in sentence case and the imperative + mood, with no body and no `Co-Authored-By` or other attribution trailers. +- Keep pull request titles to at most 64 characters. +- Write pull request descriptions as concise prose in full sentences that + explain why the change is being made and what it does. Prefer a single + paragraph, and never use bullet points, headers, test plans, or checklists. + Backticks are fine in titles and descriptions. +- Changes in behavior need tests, a `CHANGELOG.md` entry in the unreleased + section of the target branch, and an `UPGRADING.md` note when the behavior + differs between major versions. + +## Code and tooling + +- The minimum supported PHP version is 7.4, and all code must remain compatible + with it. +- PHPStan and PHP-CS-Fixer must be run against PHP 7.4. +- Keep new tests consistent with the existing tests in style and structure, and + only add tests that meaningfully cover behavior. + +## Documentation + +- Wrap markdown prose and PHPDoc text to 80 columns using greedy wrapping. Never + split a markdown link or an inline code span across a line break; a line that + cannot be broken may exceed the limit. Avoid em dashes. +- The pages under `docs/` document the public API and behavior. Keep PHPDoc and + `docs/` in sync when either changes. +- `CHANGELOG.md` follows the Keep a Changelog format, with one concise bullet + per change. + +## Security + +Never disclose security issues publicly. Follow the +[security policy](https://github.com/guzzle/command/security/policy) instead. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md From 66cfd8e1a0f515d36f3dea83d708a2b4d159eb6b Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 8 Jul 2026 22:02:31 +0100 Subject: [PATCH 2/9] Refine AGENTS.md conventions --- AGENTS.md | 62 ++++++++++++++++++++++++------------------------------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2fb8caa..3c358e8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,50 +1,42 @@ # Agent Guidelines -This file captures the repository conventions for `guzzlehttp/command`. The -organization-wide -[contributing guidelines](https://github.com/guzzle/guzzle/blob/8.0/docs/overview.md#contributing) -are required reading and apply here too. - -## Branches - -- `2.0` is the unreleased next major version and `1.5` is the current stable - branch. Bug fixes target the oldest maintained branch they apply to and are - merged up; changes are never cherry-picked down. -- Backwards compatibility on released branches is paramount. Breaking changes - are only acceptable on an unreleased major version branch. - -## Commits and pull requests - -- Write a single concise commit subject line in sentence case and the imperative - mood, with no body and no `Co-Authored-By` or other attribution trailers. -- Keep pull request titles to at most 64 characters. -- Write pull request descriptions as concise prose in full sentences that - explain why the change is being made and what it does. Prefer a single - paragraph, and never use bullet points, headers, test plans, or checklists. - Backticks are fine in titles and descriptions. -- Changes in behavior need tests, a `CHANGELOG.md` entry in the unreleased - section of the target branch, and an `UPGRADING.md` note when the behavior - differs between major versions. - ## Code and tooling +- Backwards compatibility on released branches is paramount, and breaking + changes are only acceptable on an unreleased major version branch. - The minimum supported PHP version is 7.4, and all code must remain compatible with it. - PHPStan and PHP-CS-Fixer must be run against PHP 7.4. +- Always pass an explicit character list to `trim()`, `ltrim()`, and `rtrim()`; + never rely on the default characters. +- Handle `preg_*` engine failures: when the result is used as data, test for + `false` or `null` and throw a `\RuntimeException` including + `preg_last_error_msg()`; boolean validation guards must compare strictly, such + as `=== 1`, so an engine failure can only ever fail closed. +- Anchor validation patterns to the true end of input with the `D` modifier or + `\z`; a bare `$` accepts a trailing newline. +- Never embed raw control bytes in exception messages and other diagnostics; + escape or redact the offending value first. +- Classes holding streams, resources, or callbacks reject native PHP + serialization, and refusal messages report the class name with + `static::class`. +- Reject non-finite floats where numeric values are accepted or converted to + strings. - Keep new tests consistent with the existing tests in style and structure, and only add tests that meaningfully cover behavior. +- Changes in behavior need tests, a `CHANGELOG.md` entry in the unreleased + section of the target branch, and an `UPGRADING.md` note when the behavior + differs between major versions. ## Documentation - Wrap markdown prose and PHPDoc text to 80 columns using greedy wrapping. Never split a markdown link or an inline code span across a line break; a line that cannot be broken may exceed the limit. Avoid em dashes. -- The pages under `docs/` document the public API and behavior. Keep PHPDoc and - `docs/` in sync when either changes. -- `CHANGELOG.md` follows the Keep a Changelog format, with one concise bullet - per change. - -## Security - -Never disclose security issues publicly. Follow the -[security policy](https://github.com/guzzle/command/security/policy) instead. +- PHPDoc generic types always put a space after each comma, as in + `array`. +- Keep PHPDoc and the corresponding `docs/` pages in sync: shared prose is + deliberately word-for-word identical, including boilerplate copied verbatim + between related functions, so apply the same edit to every copy. Only + formatting and linking may differ, such as a docs link becoming a PHPDoc + `@see` tag; the wording must never drift. From e24ffb5b3f043ddaf3e586bfd05ad94029c234e8 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 8 Jul 2026 22:07:06 +0100 Subject: [PATCH 3/9] Remove obvious guidance from AGENTS.md --- AGENTS.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3c358e8..4f1a760 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,11 +2,8 @@ ## Code and tooling -- Backwards compatibility on released branches is paramount, and breaking - changes are only acceptable on an unreleased major version branch. -- The minimum supported PHP version is 7.4, and all code must remain compatible - with it. -- PHPStan and PHP-CS-Fixer must be run against PHP 7.4. +- All code must remain compatible with PHP 7.4, and PHPStan and PHP-CS-Fixer + must be run against PHP 7.4. - Always pass an explicit character list to `trim()`, `ltrim()`, and `rtrim()`; never rely on the default characters. - Handle `preg_*` engine failures: when the result is used as data, test for @@ -22,19 +19,15 @@ `static::class`. - Reject non-finite floats where numeric values are accepted or converted to strings. -- Keep new tests consistent with the existing tests in style and structure, and - only add tests that meaningfully cover behavior. -- Changes in behavior need tests, a `CHANGELOG.md` entry in the unreleased - section of the target branch, and an `UPGRADING.md` note when the behavior - differs between major versions. +- Changes in behavior need a `CHANGELOG.md` entry in the unreleased section of + the target branch and an `UPGRADING.md` note when the behavior differs between + major versions. ## Documentation - Wrap markdown prose and PHPDoc text to 80 columns using greedy wrapping. Never split a markdown link or an inline code span across a line break; a line that cannot be broken may exceed the limit. Avoid em dashes. -- PHPDoc generic types always put a space after each comma, as in - `array`. - Keep PHPDoc and the corresponding `docs/` pages in sync: shared prose is deliberately word-for-word identical, including boilerplate copied verbatim between related functions, so apply the same edit to every copy. Only From b9fb35cf64f4e081f93b42430412aaefbf63ae79 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 8 Jul 2026 22:13:40 +0100 Subject: [PATCH 4/9] Add a non-finite float stringification example --- AGENTS.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4f1a760..bf37182 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,8 +17,10 @@ - Classes holding streams, resources, or callbacks reject native PHP serialization, and refusal messages report the class name with `static::class`. -- Reject non-finite floats where numeric values are accepted or converted to - strings. +- Never cast a float to string unless it is known to be finite: reject + non-finite floats where numeric values are accepted, and where one must be + stringified handle it explicitly, such as + `\is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF')`. - Changes in behavior need a `CHANGELOG.md` entry in the unreleased section of the target branch and an `UPGRADING.md` note when the behavior differs between major versions. From 61eb22bdf799ea0002981cf14b3dc2a3ddf71892 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 8 Jul 2026 22:19:21 +0100 Subject: [PATCH 5/9] Refine the non-finite float convention wording --- AGENTS.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bf37182..095fa92 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,10 +17,10 @@ - Classes holding streams, resources, or callbacks reject native PHP serialization, and refusal messages report the class name with `static::class`. -- Never cast a float to string unless it is known to be finite: reject - non-finite floats where numeric values are accepted, and where one must be - stringified handle it explicitly, such as - `\is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF')`. +- In general, numeric inputs should not accept non-finite floats. In situations + where they are accepted and we need to cast to a string, we should branch on + `\is_finite($value)`, using `(string) $value` for the finite case and + `\is_nan($value) ? 'NAN' : ($value > 0 ? 'INF' : '-INF')` otherwise. - Changes in behavior need a `CHANGELOG.md` entry in the unreleased section of the target branch and an `UPGRADING.md` note when the behavior differs between major versions. From 407df95d5e299693fdc89646d1da005df3fd8a79 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 8 Jul 2026 22:23:05 +0100 Subject: [PATCH 6/9] Detail the serialization hardening convention --- AGENTS.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 095fa92..27b8c64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,9 +14,14 @@ `\z`; a bare `$` accepts a trailing newline. - Never embed raw control bytes in exception messages and other diagnostics; escape or redact the offending value first. -- Classes holding streams, resources, or callbacks reject native PHP - serialization, and refusal messages report the class name with - `static::class`. +- Classes holding streams, resources, handles, callbacks, or credentials reject + native PHP serialization so they cannot join object-injection gadget chains: + `__serialize()` and `__unserialize()` both throw + `\LogicException(static::class.' should never be serialized')` and its + unserialized counterpart, usually via the repo's `@internal` non-serializable + trait. Guards that protect a `__destruct()` gadget also neutralize the + dangerous state before throwing, so the protection holds even if the exception + is swallowed. - In general, numeric inputs should not accept non-finite floats. In situations where they are accepted and we need to cast to a string, we should branch on `\is_finite($value)`, using `(string) $value` for the finite case and From 44df714ce237d020cd289408a407fbd41d558a19 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 8 Jul 2026 22:25:59 +0100 Subject: [PATCH 7/9] Expand serialization and static helper conventions --- AGENTS.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 27b8c64..22445a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,14 +14,20 @@ `\z`; a bare `$` accepts a trailing newline. - Never embed raw control bytes in exception messages and other diagnostics; escape or redact the offending value first. -- Classes holding streams, resources, handles, callbacks, or credentials reject - native PHP serialization so they cannot join object-injection gadget chains: - `__serialize()` and `__unserialize()` both throw +- Helper classes that expose only public static methods are `final` and have a + private constructor. +- Resist native PHP serialization when a class holds live state (streams, + resources, handles, callbacks, credentials) or when magic methods such as + `__destruct()` have side effects that unserialized attacker-controlled state + could redirect, as with the file and session write gadgets fixed in Guzzle's + persisting cookie jars. Plain data holders, such as Guzzle's in-memory cookie + jar, remain serializable. +- To resist, `__serialize()` and `__unserialize()` both throw `\LogicException(static::class.' should never be serialized')` and its unserialized counterpart, usually via the repo's `@internal` non-serializable - trait. Guards that protect a `__destruct()` gadget also neutralize the - dangerous state before throwing, so the protection holds even if the exception - is swallowed. + trait. Where a `__destruct()` gadget exists, the side effect is armed only by + the constructor and disarmed in `__wakeup()` and `__unserialize()` before + throwing, so the defense holds even if the exception is swallowed. - In general, numeric inputs should not accept non-finite floats. In situations where they are accepted and we need to cast to a string, we should branch on `\is_finite($value)`, using `(string) $value` for the finite case and From 9a9a59bbddf128fc21c8794339c77042bce7a394 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 8 Jul 2026 22:30:26 +0100 Subject: [PATCH 8/9] Reword the serialization conventions --- AGENTS.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 22445a9..c9cb9ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,16 +18,16 @@ private constructor. - Resist native PHP serialization when a class holds live state (streams, resources, handles, callbacks, credentials) or when magic methods such as - `__destruct()` have side effects that unserialized attacker-controlled state - could redirect, as with the file and session write gadgets fixed in Guzzle's - persisting cookie jars. Plain data holders, such as Guzzle's in-memory cookie - jar, remain serializable. + `__destruct()` have side effects that untrusted unserialized data could + redirect, as with the file and session write fixes in Guzzle's persisting + cookie jars. Plain data holders, such as Guzzle's in-memory cookie jar, remain + serializable. - To resist, `__serialize()` and `__unserialize()` both throw `\LogicException(static::class.' should never be serialized')` and its unserialized counterpart, usually via the repo's `@internal` non-serializable - trait. Where a `__destruct()` gadget exists, the side effect is armed only by - the constructor and disarmed in `__wakeup()` and `__unserialize()` before - throwing, so the defense holds even if the exception is swallowed. + trait. Where a `__destruct()` has such a side effect, it is enabled only by + the constructor and disabled in `__wakeup()` and `__unserialize()` before + throwing, so the protection holds even if the exception is swallowed. - In general, numeric inputs should not accept non-finite floats. In situations where they are accepted and we need to cast to a string, we should branch on `\is_finite($value)`, using `(string) $value` for the finite case and From 092c546f9e1274cef87c63a78de168fafa1ef2ca Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Wed, 8 Jul 2026 22:33:42 +0100 Subject: [PATCH 9/9] Scope serialization guidance per repo --- AGENTS.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c9cb9ae..8ce3ee5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,17 +17,11 @@ - Helper classes that expose only public static methods are `final` and have a private constructor. - Resist native PHP serialization when a class holds live state (streams, - resources, handles, callbacks, credentials) or when magic methods such as - `__destruct()` have side effects that untrusted unserialized data could - redirect, as with the file and session write fixes in Guzzle's persisting - cookie jars. Plain data holders, such as Guzzle's in-memory cookie jar, remain + resources, handles, callbacks, credentials); plain data holders remain serializable. - To resist, `__serialize()` and `__unserialize()` both throw `\LogicException(static::class.' should never be serialized')` and its - unserialized counterpart, usually via the repo's `@internal` non-serializable - trait. Where a `__destruct()` has such a side effect, it is enabled only by - the constructor and disabled in `__wakeup()` and `__unserialize()` before - throwing, so the protection holds even if the exception is swallowed. + unserialized counterpart, via the `@internal` `NonSerializableTrait`. - In general, numeric inputs should not accept non-finite floats. In situations where they are accepted and we need to cast to a string, we should branch on `\is_finite($value)`, using `(string) $value` for the finite case and