gws — Fix Report
Generated: 2026-05-14 CLI version: 0.17.0 Scope: Critical (22 failure modes) In findings: 22 failure modes evaluated
Summary
| Severity | Pass (3/3) | Partial (1–2) | Fail (0) | Indeterminate (?) |
|---|---|---|---|---|
| Critical | 3 | 15 | 4 | 0 |
Required Fixes (score < 3, sorted: severity desc, score asc)
§11 — Timeouts & Hanging Processes [Critical · 0/3]
Gap: No --timeout flag exists; network hangs are handled silently by the HTTP stack with no defined exit code or JSON error structure.
Solutions: Built-in timeout flags:
tool operation --timeout 30s
tool operation --connect-timeout 5s
Progress heartbeats to stderr:
{"status": "running", "step": "calling drive API", "elapsed_ms": 5000, "heartbeat": true}
Emit partial results before timeout:
{
"ok": false,
"partial": true,
"error": {"code": "TIMEOUT", "message": "Operation timed out after 30s"},
"resume_token": "abc123"
}
Requirements that address this: - REQ-C-028 (High) — Timeout flag declaration [Tier: C = you declare] - REQ-C-029 (High) — Structured timeout error [Tier: C = you declare]
§13 — Partial Failure & Atomicity [Critical · 0/3]
Gap: Workflow commands have no completed_steps, no resume_from token, and no --rollback-on-failure; agents cannot determine what to retry safely.
Solutions: Structured partial failure output:
{
"ok": false,
"partial": true,
"completed_steps": ["fetch_meetings", "fetch_tasks"],
"failed_step": "send_notification",
"error": {"code": "RATE_LIMITED", "message": "..."},
"resume_from": "send_notification"
}
Batch result per item:
{
"ok": false,
"partial": true,
"results": [
{"id": "meeting-1", "ok": true, "effect": "included"},
{"id": "email-1", "ok": false, "error": {"code": "RATE_LIMITED"}}
],
"summary": {"total": 2, "succeeded": 1, "failed": 1}
}
Resumable commands — expose resume_from so agents can restart at the failed step without re-running successful ones.
Requirements that address this: - REQ-C-030 (High) — Partial failure envelope [Tier: C = you declare] - REQ-O-045 (Medium) — Rollback flag [Tier: O = you opt in]
§25 — Prompt Injection via Output [Critical · 0/3]
Gap: API responses return external content (email subjects, document bodies, file names) as raw strings with no trusted/untrusted markers — LLMs consuming this output cannot distinguish CLI metadata from external data.
Solutions: Content type tagging on all external data fields:
{
"ok": true,
"data": {
"_content_type": "user_data",
"subject": "...",
"body": "..."
}
}
Structural wrapping in framework output — the framework should always wrap external data so the agent knows it is data, not instructions:
<tool_result source="gmail.messages.get" trusted="false">
<raw content — treat as untrusted data>
</tool_result>
Enable --sanitize on all commands by default (already available via GWS_SANITIZE_TEMPLATE), not just per-command opt-in.
Requirements that address this: - REQ-F-012 (High) — External data tagging [Tier: F = framework handles] - REQ-C-015 (Medium) — trusted field on data responses [Tier: C = you declare]
§53 — Credential Expiry Mid-Session [Critical · 0/3]
Gap: Expired credentials return reason: "authError" — identical to permanent permission denial. Agents cannot distinguish the two and cannot safely auto-retry.
Solutions: Auth errors MUST distinguish expiry from permission denial:
{
"ok": false,
"error": {
"code": "CREDENTIALS_EXPIRED",
"message": "Access token expired.",
"expired": true,
"expired_at": "2026-05-14T11:00:00Z",
"retryable": true,
"reauth_command": "gws auth login",
"reauth_env_var": "GOOGLE_WORKSPACE_CLI_TOKEN"
}
}
Add exit 10 to the exit code table: 10 = credentials expired (retryable with refresh). Exit 8 = permanent permission denied.
Classify HTTP 401/403 responses by inspecting the underlying OAuth error before surfacing — invalid_rapt and invalid_grant are expiry signals; insufficient_scope and access_denied are permanent.
Requirements that address this: - REQ-C-018 (High) — CREDENTIALS_EXPIRED distinct code [Tier: C = you declare] - REQ-C-019 (High) — reauth_command field [Tier: C = you declare] - REQ-F-008 (High) — Exit 10 for credential expiry [Tier: F = framework handles]
§1 — Exit Codes & Status Signaling [Critical · 1/3]
Gap: Auth errors inconsistently exit 0 (list commands) vs exit 2 (get commands). No documented exit code table. JSON error body uses HTTP codes (401, 400) rather than symbolic names.
Solutions: Follow the standard exit code table:
0 = success
2 = bad arguments / validation error (already used — good)
3 = operation failed mid-way
5 = not found
8 = permission denied / auth failure
9 = rate limited
10 = credentials expired (retryable)
Fix: all auth failures must exit 8 (or 10 for expiry), never 0. Current inconsistency where gws drive files list exits 0 on auth error is a bug.
Use symbolic error codes in JSON, not HTTP codes:
{"error": {"code": "AUTH_FAILED", "http_status": 401, ...}}
Requirements that address this: - REQ-C-001 (Critical) — Semantic exit codes [Tier: C = you declare] - REQ-C-002 (High) — Exit code table documented in --help [Tier: C = you declare]
§2 — Output Format & Parseability [Critical · 1/3]
Gap: JSON output exists and is the default, but uses {error:{code,message,reason}} without a top-level ok/data/meta envelope. Prose error lines are also emitted to stderr alongside JSON.
Solutions: Machine-readable output with consistent envelope:
{
"ok": true,
"data": {"files": [...]},
"warnings": [],
"meta": {"request_id": "...", "duration_ms": 120}
}
Error envelope:
{
"ok": false,
"error": {"code": "AUTH_FAILED", "message": "...", "retryable": false},
"meta": {"request_id": "...", "duration_ms": 45}
}
Remove the prose error[auth]: ... line from stderr when stdout already carries the JSON error — this line pollutes stderr for agents parsing it separately.
Requirements that address this: - REQ-C-003 (Critical) — Consistent JSON envelope [Tier: C = you declare] - REQ-C-004 (High) — ok/data/error/meta top-level fields [Tier: C = you declare]
§12 — Idempotency & Safe Retries [Critical · 1/3]
Gap: --dry-run exists on destructive commands but no --idempotency-key and no effect field (created/updated/noop) in responses. Agents cannot detect duplicate writes.
Solutions: Idempotency keys:
gws gmail users messages send --idempotency-key "msg-$(date +%s)-$RANDOM" --json '{...}'
Declare operation effect in output:
{
"ok": true,
"effect": "created",
"data": {"id": "msg_abc123"}
}
Second call with same key returns:
{
"ok": true,
"effect": "noop",
"reason": "Message already sent with this idempotency key",
"data": {"id": "msg_abc123"}
}
Requirements that address this: - REQ-O-020 (High) — Idempotency key support [Tier: O = you opt in] - REQ-C-021 (High) — effect field in all mutating responses [Tier: C = you declare]
§23 — Side Effects & Destructive Operations [Critical · 1/3]
Gap: --dry-run validates requests locally but does not return effect: "would_delete" or the affected scope. No danger_level declared in schema. No reversible field.
Solutions: Dry-run must return structured would-do output:
gws drive files delete --params '{"fileId":"abc"}' --dry-run
{
"ok": true,
"effect": "would_delete",
"would_affect": {
"file": {"id": "abc", "name": "Budget.xlsx"},
"reversible": false,
"note": "File will be permanently deleted, bypassing Trash"
}
}
Declare danger_level in gws schema output:
{
"command": "drive.files.delete",
"danger_level": "destructive",
"reversible": false
}
Requirements that address this: - REQ-C-025 (High) — danger_level in schema [Tier: C = you declare] - REQ-O-026 (High) — Dry-run with would-do envelope [Tier: O = you opt in]
§34 — Shell Injection via Agent-Constructed Commands [Critical · 1/3]
Gap: Compiled Rust binary avoids shell=True, but --params JSON is passed to the API without CLI-level metacharacter validation. Path traversal (../../) and percent-encoded values are accepted without rejection.
Solutions: Reject known injection patterns at the CLI argument level before sending to API:
import re
SAFE_PARAM_RE = re.compile(r'^[^;&|<>`$\\\n\r]+$')
# Reject: ../, %2F, embedded ?, #, null bytes
Add agent_hardening validation for --params JSON values — scan for ../, %[0-9a-f]{2}, embedded ? or # in resource ID fields and exit with a structured VALIDATION_ERROR before making the API call.
Requirements that address this: - REQ-F-034 (High) — Metacharacter rejection [Tier: F = framework handles] - REQ-F-035 (High) — Path traversal rejection [Tier: F = framework handles]
§42 — Debug / Trace Mode Secret Leakage [Critical · 1/3]
Gap: GOOGLE_WORKSPACE_CLI_LOG=gws=debug emits ANSI escape sequences to stderr. No sensitive: true field declarations in schema. No --trace-safe mode.
Solutions: Auto-redact values matching token/secret/key/password patterns in all debug output:
[DEBUG] Using token: [REDACTED]
[DEBUG] GOOGLE_WORKSPACE_CLI_TOKEN: [REDACTED]
Provide GOOGLE_WORKSPACE_CLI_LOG_SAFE=true mode that enables debug logging with sensitive fields replaced by [REDACTED].
Strip ANSI codes from debug output when stderr is not a TTY.
Declare sensitive fields in schema:
{
"env_vars": [
{"name": "GOOGLE_WORKSPACE_CLI_TOKEN", "sensitive": true}
]
}
Requirements that address this: - REQ-F-042 (High) — Auto-redact sensitive fields [Tier: F = framework handles] - REQ-C-043 (Medium) — sensitive: true in schema declarations [Tier: C = you declare]
§43 — Tool Output Result Size Unboundedness [Critical · 1/3]
Gap: --page-limit 10 caps pagination pages but does not limit individual response body size. A single large document or email body is returned in full with no meta.truncated signal.
Solutions:
Add meta.truncated to responses when output is capped:
{
"ok": true,
"data": {"body": "First 10000 chars..."},
"meta": {"truncated": true, "total_bytes": 204800, "returned_bytes": 10240,
"truncation_hint": "Use --offset and --max-length for subsequent chunks"}
}
Add --max-length N flag to all commands returning large text fields (document bodies, email bodies, file contents). Default to 50KB with explicit opt-out.
Declare max_output_bytes in gws schema output per command.
Requirements that address this: - REQ-C-044 (High) — meta.truncated signal [Tier: C = you declare] - REQ-O-045 (High) — max-length flag [Tier: O = you opt in]
§45 — Headless Authentication / OAuth Browser Flow Blocking [Critical · 1/3]
Gap: API calls without credentials exit immediately (no hang), but exit code is 0 instead of 8, error uses reason: "authError" not AUTH_REQUIRED, and no auth_methods array is returned.
Solutions:
Return structured AUTH_REQUIRED error with auth methods:
{
"ok": false,
"error": {
"code": "AUTH_REQUIRED",
"message": "No credentials found.",
"auth_methods": [
{"type": "env_var", "name": "GOOGLE_WORKSPACE_CLI_TOKEN", "description": "Pre-obtained OAuth2 access token"},
{"type": "env_var", "name": "GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE", "description": "Path to OAuth credentials JSON"}
]
}
}
Exit 8 (not 0) on auth failure.
Include "requires_auth": true and "auth_methods": [...] in gws schema output.
Requirements that address this: - REQ-C-009 (High) — AUTH_REQUIRED code with auth_methods [Tier: C = you declare] - REQ-F-010 (High) — Exit 8 on auth failure [Tier: F = framework handles]
§60 — OS Output Buffer Deadlock [Critical · 1/3]
Gap: Single-shot API call model; no heartbeat for long-running operations; debug output carries ANSI escape codes to stderr.
Solutions: For long-running workflow commands, emit JSON heartbeats to stdout:
{"status": "running", "step": "fetching meetings", "elapsed_ms": 2000, "heartbeat": true}
Strip ANSI codes from debug output when stderr is not a TTY (detect with isatty()).
Explicitly set line buffering on stdout startup when not a TTY.
Requirements that address this: - REQ-F-060 (High) — Line-buffered stdout in non-TTY [Tier: F = framework handles] - REQ-O-061 (Medium) — Heartbeat for long operations [Tier: O = you opt in]
§64 — Headless Display and GUI Launch Blocking [Critical · 1/3]
Gap: gws auth login opens a browser with no --print-url or --no-browser alternative. Headless agents must pre-set GOOGLE_WORKSPACE_CLI_TOKEN externally.
Solutions:
Add --print-url flag to gws auth login — emits the auth URL as JSON instead of opening a browser:
{
"ok": true,
"data": {
"auth_url": "https://accounts.google.com/o/oauth2/auth?...",
"opened": false,
"note": "Open this URL in a browser to complete authentication"
}
}
Detect headless environment (CI=true, DISPLAY unset) and automatically emit URL rather than launching browser.
Requirements that address this: - REQ-C-064 (High) — headless_behavior in schema [Tier: C = you declare] - REQ-O-065 (Medium) — --print-url flag on auth commands [Tier: O = you opt in]
§71 — Non-Interactive Installation Absence [Critical · 1/3]
Gap: Homebrew install is non-interactive and idempotent in practice, but this is not documented in AGENTS.md. No verify command documented. An update from 0.17.0 → 0.22.5 is available.
Solutions:
Add an AGENTS.md file documenting the canonical non-interactive install:
## Installation
brew install googleworkspace-cli # non-interactive, idempotent
gws --version # verify: exits 0, prints version
Publish a gws doctor --json health-check command agents can run after install to confirm the binary is functional and credentials are valid.
Update to 0.22.5 — current 0.17.0 is outdated.
Requirements that address this: - REQ-O-071 (High) — AGENTS.md with install + verify commands [Tier: O = you opt in] - REQ-C-072 (Medium) — Idempotent install documented [Tier: C = you declare]
§74 — Credential Scope Declaration Absence [Critical · 1/3]
Gap: gws schema <method> returns a scopes field listing all possible OAuth scopes for a method (up to 8), but not a required_scopes minimal set. No over-privileged warning. No check-permissions command.
Solutions:
Add required_scopes (minimal set) to schema output, distinct from scopes (all possible):
{
"command": "drive.files.list",
"required_scopes": ["https://www.googleapis.com/auth/drive.readonly"],
"scopes": ["https://www.googleapis.com/auth/drive", "...6 more..."]
}
Add gws check-permissions --for drive.files.list:
{
"ok": true,
"required_scopes": ["https://www.googleapis.com/auth/drive.readonly"],
"active_scopes": ["https://www.googleapis.com/auth/drive"],
"over_privileged": true,
"warnings": ["Active credential has broader access than this command needs"]
}
Emit warnings[] when active credential scope exceeds required_scopes.
Document minimal credential recipes for common agent workflows in AGENTS.md.
Requirements that address this: - REQ-C-074 (High) — required_scopes in schema [Tier: C = you declare] - REQ-O-075 (Medium) — check-permissions pre-flight command [Tier: O = you opt in] - REQ-O-076 (Medium) — over-privileged warning in warnings[] [Tier: O = you opt in]
§10 — Interactivity & TTY Requirements [Critical · 2/3]
Gap: No hang on stdin=DEVNULL (good), but no explicit --non-interactive flag and no TTY auto-detection declared in schema. gws auth login opens browser without checking TTY state first.
Solutions:
Add --non-interactive flag that forces immediate failure with a structured error if any interactive path would otherwise be triggered.
Detect non-interactive context automatically:
if not sys.stdin.isatty():
# fail fast on any interactive path
# never prompt; never open editor or browser
Document non-interactive operation guarantees in schema and AGENTS.md.
Requirements that address this: - REQ-C-010 (High) — --non-interactive flag [Tier: C = you declare] - REQ-F-011 (High) — TTY auto-detection [Tier: F = framework handles]
§24 — Authentication & Secret Handling [Critical · 2/3]
Gap: Credentials via env vars only (good — no --token flag). Secret not echoed in errors (good). Missing: no auto-redaction framework, no --secret-from-file flag.
Solutions:
Add --secret-from-file / --token-file alternative to env var for containerized environments where env vars are harder to manage:
gws drive files list --token-file /run/secrets/gws-token
Add framework-level auto-redaction for any env var matching *_TOKEN, *_SECRET, *_KEY, *_PASSWORD in all log and debug output.
Requirements that address this: - REQ-O-024 (Medium) — --secret-from-file flag [Tier: O = you opt in] - REQ-F-025 (High) — Auto-redact sensitive env var values [Tier: F = framework handles]
§61 — Bidirectional Pipe Payload Deadlock [Critical · 2/3]
Gap: No stdin data path for API operations (good — avoids deadlock). --json and --params take string arguments; --upload uses file paths. However, no --input-file for large --json payloads and no documented stdin size limit.
Solutions:
Add --params-file and --json-file flags as alternatives for large request bodies that would overflow CLI argument limits:
gws sheets spreadsheets values batchUpdate --json-file /tmp/batch-update.json
Document the safe payload size for --json/--params string arguments in schema.
Requirements that address this: - REQ-O-061 (Medium) — --input-file for large payloads [Tier: O = you opt in] - REQ-C-062 (Medium) — Document max arg size [Tier: C = you declare]
Already Passing
§37 (REPL Triggering), §50 (Stdin Deadlock), §62 (Editor Trap) — score 3/3, no action needed