MCPcopy
hub / github.com/codeaashu/claude-code / checkSemantics

Function checkSemantics

src/utils/bash/ast.ts:2213–2679  ·  view source on GitHub ↗
(commands: SimpleCommand[])

Source from the content-addressed store, hash-verified

2211 * content. Returns the first failure or {ok: true}.
2212 */
2213export function checkSemantics(commands: SimpleCommand[]): SemanticCheckResult {
2214 for (const cmd of commands) {
2215 // Strip safe wrapper commands (nohup, time, timeout N, nice -n N) so
2216 // `nohup eval "..."` and `timeout 5 jq 'system(...)'` are checked
2217 // against the wrapped command, not the wrapper. Inlined here to avoid
2218 // circular import with bashPermissions.ts.
2219 let a = cmd.argv
2220 for (;;) {
2221 if (a[0] === 'time' || a[0] === 'nohup') {
2222 a = a.slice(1)
2223 } else if (a[0] === 'timeout') {
2224 // `timeout 5`, `timeout 5s`, `timeout 5.5`, plus optional GNU flags
2225 // preceding the duration. Long: --foreground, --kill-after=N,
2226 // --signal=SIG, --preserve-status. Short: -k DUR, -s SIG, -v (also
2227 // fused: -k5, -sTERM).
2228 // SECURITY (SAST Mar 2026): the previous loop only skipped `--long`
2229 // flags, so `timeout -k 5 10 eval ...` broke out with name='timeout'
2230 // and the wrapped eval was never checked. Now handle known short
2231 // flags AND fail closed on any unrecognized flag — an unknown flag
2232 // means we can't locate the wrapped command, so we must not silently
2233 // fall through to name='timeout'.
2234 let i = 1
2235 while (i < a.length) {
2236 const arg = a[i]!
2237 if (
2238 arg === '--foreground' ||
2239 arg === '--preserve-status' ||
2240 arg === '--verbose'
2241 ) {
2242 i++ // known no-value long flags
2243 } else if (/^--(?:kill-after|signal)=[A-Za-z0-9_.+-]+$/.test(arg)) {
2244 i++ // --kill-after=5, --signal=TERM (value fused with =)
2245 } else if (
2246 (arg === '--kill-after' || arg === '--signal') &&
2247 a[i + 1] &&
2248 /^[A-Za-z0-9_.+-]+$/.test(a[i + 1]!)
2249 ) {
2250 i += 2 // --kill-after 5, --signal TERM (space-separated)
2251 } else if (arg.startsWith('--')) {
2252 // Unknown long flag, OR --kill-after/--signal with non-allowlisted
2253 // value (e.g. placeholder from $() substitution). Fail closed.
2254 return {
2255 ok: false,
2256 reason: `timeout with ${arg} flag cannot be statically analyzed`,
2257 }
2258 } else if (arg === '-v') {
2259 i++ // --verbose, no argument
2260 } else if (
2261 (arg === '-k' || arg === '-s') &&
2262 a[i + 1] &&
2263 /^[A-Za-z0-9_.+-]+$/.test(a[i + 1]!)
2264 ) {
2265 i += 2 // -k DURATION / -s SIGNAL — separate value
2266 } else if (/^-[ks][A-Za-z0-9_.+-]+$/.test(arg)) {
2267 i++ // fused: -k5, -sTERM
2268 } else if (arg.startsWith('-')) {
2269 // Unknown flag OR -k/-s with non-allowlisted value — can't locate
2270 // wrapped cmd. Reject, don't fall through to name='timeout'.

Callers 1

bashToolHasPermissionFunction · 0.85

Calls 1

hasMethod · 0.45

Tested by

no test coverage detected