claude-agent-sdk 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a12b68451568e5428820499fb4bf42401596477ea74d6b25a8fad3637b70178
4
- data.tar.gz: d0465a73a3cbe17d3775d65d83978c16527548f9f752f8786b606f2c8c1c3713
3
+ metadata.gz: c2722d8b996148ef678484d587c1fcd88aaa68d9f3a380b342ad81b784f558cc
4
+ data.tar.gz: ce8a7d155a9c21fcddd4ad4dee0ec2b728d5397bb378d0b4c53f867febbc9cf0
5
5
  SHA512:
6
- metadata.gz: 8051e5090a5361c74eb67506cec3021c69da07e9150b41cce91c674af759f7f03a3b9547e08abf0297f17bd32441cc3d04e1ea62fb92c47168841fd9991e5669
7
- data.tar.gz: c70f056865cd07758f372b35bf7e8a14571f88b4ae4189c41ef26309a637068593293d1057aba021c8c4d184821a384962e7547048d66d2c615090067d796231
6
+ metadata.gz: 4a73b55a0228259e19e847683c4acccc6ac319f355f7d7fe9c8fd01dbc642e6ff7de767217b770dd95b8e3e847f280f46b49a7888f534278529b576412c29a7a
7
+ data.tar.gz: 5a1012c3ebd9a59f2a97476210b41873d616f053e7c02ba7c2bb385968f8134fb13b9f13730bb0c404b41877cc169dbde7c94f2d87b461671377492934511d0f
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.29.0] - 2026-08-09
11
+
12
+ ### Fixed
13
+ - **Skill names in `ClaudeAgentOptions#skills` are now validated** (port of Python SDK [#1145](https://github.com/anthropics/claude-agent-sdk-python/pull/1145), v0.2.129). Names were formatted into the `--allowedTools` value unchecked; the CLI splits that value into permission rules on commas and spaces outside parentheses with no escape sequences, so a name carrying a delimiter could not be passed through reliably. The command builder now validates each name and fails closed with `ArgumentError` (`TypeError` for non-String entries). Rejected: parentheses, commas, control characters (C0, DEL, C1), U+FEFF, empty names; a literal `*` and wildcard suffixes (`:*`, ` *`); and shapes that parse but can never match the listed skill — surrounding whitespace (Unicode-aware, unlike `String#strip`), a leading `/`, consecutive backslashes, a trailing unpaired backslash, and byte sequences that cannot form valid UTF-8 (Ruby's analogue of Python's surrogate check). Ordinary names are unaffected, including plugin-qualified names, interior spaces, single backslashes, and non-ASCII; valid non-UTF-8 strings are converted so the argv join cannot raise `Encoding::CompatibilityError`.
14
+ - **Breaking:** `skills: ['*']` and `skills: ['plugin:*']` now raise — use `skills: 'all'`, or a `Skill(...)` rule in `allowed_tools` for prefix matching. `skills: [' name']` and `skills: ['/name']` now raise as well; both previously built a rule that could never match, so the skill was silently unavailable.
15
+
10
16
  ## [0.28.0] - 2026-07-31
11
17
 
12
18
  ### Changed
data/README.md CHANGED
@@ -68,7 +68,7 @@ Add this line to your application's Gemfile:
68
68
  gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
69
69
 
70
70
  # Or use a stable version from RubyGems
71
- gem 'claude-agent-sdk', '~> 0.28.0'
71
+ gem 'claude-agent-sdk', '~> 0.29.0'
72
72
  ```
73
73
 
74
74
  Then `bundle install`, or install directly: `gem install claude-agent-sdk`.
@@ -9,6 +9,16 @@ module ClaudeAgentSDK
9
9
  class CommandBuilder
10
10
  EXTRA_ARG_FLAG_REGEXP = /\A[a-z0-9][a-z0-9-]*\z/
11
11
 
12
+ # Parentheses and commas are delimiters to the --allowedTools tokenizer;
13
+ # control characters (C0, DEL, C1) never appear in a skill directory name.
14
+ # U+FEFF is here rather than in the whitespace checks below because the
15
+ # CLI trims it as whitespace and [[:space:]] does not match it.
16
+ SKILL_NAME_INVALID_CHARS = /[(),\u0000-\u001F\u007F-\u009F\uFEFF]/
17
+
18
+ # Unicode-aware edge whitespace: the CLI and Skill tool trim Unicode
19
+ # whitespace, and Ruby's String#strip is ASCII-only.
20
+ SKILL_NAME_EDGE_WHITESPACE = /\A[[:space:]]+|[[:space:]]+\z/
21
+
12
22
  def initialize(cli_path, options)
13
23
  @cli_path = cli_path
14
24
  @options = options
@@ -93,26 +103,89 @@ module ClaudeAgentSDK
93
103
  # entry, no duplicates) and default setting_sources to user+project so
94
104
  # skill files are actually discovered. Explicit setting_sources (including
95
105
  # []) is never overridden. Non-mutating; returns the effective pair.
96
- # Justified divergence: a non-'all' String raises (NoMethodError on #each)
97
- # instead of Python's quirk of iterating its characters.
106
+ # Each listed name is validated before being formatted into a rule (see
107
+ # #validate_skill_name). Both SDKs reject non-list, non-'all' shapes
108
+ # loudly; this raises ArgumentError where Python raises TypeError.
98
109
  def skills_defaults
99
110
  allowed_tools = @options.allowed_tools.dup
100
111
  setting_sources = @options.setting_sources&.dup
101
112
  skills = @options.skills
102
113
  return [allowed_tools, setting_sources] if skills.nil?
103
114
 
104
- # Fail loudly with a clear message instead of a bare NoMethodError from
105
- # deep inside build for skills: :all / 'pdf' / Hash typos (and instead
106
- # of Python's quirk of iterating a String's characters).
107
115
  valid = skills == "all" || skills.is_a?(Array)
108
116
  raise ArgumentError, "skills must be 'all' or an Array of skill names (got #{skills.inspect})" unless valid
109
117
 
110
- entries = skills == "all" ? ["Skill"] : skills.map { |name| "Skill(#{name})" }
118
+ entries = skills == "all" ? ["Skill"] : skills.map { |name| "Skill(#{validate_skill_name(name)})" }
111
119
  entries.each { |entry| allowed_tools << entry unless allowed_tools.include?(entry) }
112
120
  setting_sources = %w[user project] if setting_sources.nil?
113
121
  [allowed_tools, setting_sources]
114
122
  end
115
123
 
124
+ # Reject skill names that cannot ride safely in a Skill(name) rule
125
+ # (port of Python SDK #1145). Names from options.skills are formatted
126
+ # into the --allowedTools value, which the CLI splits into rules on
127
+ # commas and spaces outside parentheses. That tokenizer honors no escape
128
+ # sequences -- escaping exists only in the per-rule grammar, applied
129
+ # after splitting -- so a name carrying a delimiter cannot be passed
130
+ # through reliably: what it tokenizes into depends on what surrounds it.
131
+ #
132
+ # Names that tokenize cleanly but can never match the listed skill are
133
+ # rejected too, so a dead rule fails loudly here instead of silently
134
+ # granting nothing. Returns the name as a UTF-8 String so later argv
135
+ # joins cannot raise Encoding::CompatibilityError.
136
+ def validate_skill_name(name) # rubocop:disable Metrics/MethodLength
137
+ raise TypeError, "Skill names must be strings, got #{name.class}: #{name.inspect}" unless name.is_a?(String)
138
+
139
+ # Ruby's analogue of Python's surrogate check: a lone surrogate (or any
140
+ # other broken byte sequence) is unrepresentable in valid UTF-8, and no
141
+ # CLI-discovered skill name contains one.
142
+ utf8 = begin
143
+ name.encode(Encoding::UTF_8)
144
+ rescue EncodingError
145
+ nil
146
+ end
147
+ if utf8.nil? || !utf8.valid_encoding?
148
+ raise ArgumentError, "Invalid skill name #{name.inspect}: contains bytes that cannot form " \
149
+ "valid UTF-8 (such as a surrogate code point), which can never match " \
150
+ "a skill the CLI discovered."
151
+ end
152
+
153
+ stripped = utf8.gsub(SKILL_NAME_EDGE_WHITESPACE, "")
154
+ raise ArgumentError, "Skill names must be non-empty strings" if stripped.empty?
155
+
156
+ if utf8 != stripped
157
+ raise ArgumentError, "Invalid skill name #{name.inspect}: leading or trailing whitespace " \
158
+ "can never match -- the Skill tool trims the invoked name."
159
+ end
160
+ if SKILL_NAME_INVALID_CHARS.match?(utf8)
161
+ raise ArgumentError, "Invalid skill name #{name.inspect}: parentheses, commas, control " \
162
+ "characters, and byte-order marks are not allowed. Names match the " \
163
+ "skill's directory name, or 'plugin:skill' for plugin-qualified skills."
164
+ end
165
+ raise ArgumentError, "Invalid skill name '*': use skills: 'all' to enable every skill." if utf8 == "*"
166
+
167
+ if utf8.end_with?(":*", " *")
168
+ raise ArgumentError, "Invalid skill name #{name.inspect}: wildcard-suffix names are not " \
169
+ "allowed; list each skill by its exact name."
170
+ end
171
+ if utf8.start_with?("/")
172
+ raise ArgumentError, "Invalid skill name #{name.inspect}: skill names may not start with " \
173
+ "'/'. The skills option takes the canonical name, not the " \
174
+ "slash-command form."
175
+ end
176
+ if utf8.include?("\\\\")
177
+ raise ArgumentError, "Invalid skill name #{name.inspect}: consecutive backslashes are not " \
178
+ "allowed -- the per-rule parser collapses them, so the rule would " \
179
+ "name a different skill."
180
+ end
181
+ if utf8.end_with?("\\")
182
+ raise ArgumentError, "Invalid skill name #{name.inspect}: names may not end with an " \
183
+ "unpaired backslash."
184
+ end
185
+
186
+ utf8
187
+ end
188
+
116
189
  def append_disallowed_tools(cmd)
117
190
  cmd.push("--disallowedTools", @options.disallowed_tools.join(",")) unless @options.disallowed_tools.empty?
118
191
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '0.28.0'
4
+ VERSION = '0.29.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude-agent-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.28.0
4
+ version: 0.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Community Contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-31 00:00:00.000000000 Z
11
+ date: 2026-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async