Skip to content

fix: session cost from stdin and delegation hook bypass#39

Merged
barkain merged 6 commits into
mainfrom
fix/statusline-cost-and-hook-bypass
Apr 6, 2026
Merged

fix: session cost from stdin and delegation hook bypass#39
barkain merged 6 commits into
mainfrom
fix/statusline-cost-and-hook-bypass

Conversation

@barkain

@barkain barkain commented Apr 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace broken daily cost calculation with session cost from Claude Code's stdin JSON
  • Harden delegation hook bypass to prevent subagent deadlocks

Cost Display

  • Removed: 445 lines of JSONL self-calculation code (9 functions, background refresh, cache files)
  • Added: 5-line read of cost.total_cost_usd from stdin JSON — accurate, zero I/O
  • Root cause: _find_todays_session_files() used file mtime, picking up old sessions touched today and summing their entire history ($1505 instead of actual daily cost)

Delegation Hook

  • Added: 3 env var checks for subagent detection (CLAUDE_PARENT_SESSION_ID, CLAUDE_AGENT_ID, CLAUDE_SCRATCHPAD_DIR)
  • Fixed: get_state_dir() now uses Path.resolve() to normalize symlinks when checking delegation_active flag
  • Added: Redundant safety net inside main() rechecking subagent env vars

Test plan

  • Statusline shows reasonable session cost (not $1505)
  • Subagents can use tools without being blocked by delegation hook
  • Delegation enforcement still works at top level

🤖 Generated with Claude Code

@qodo-code-review

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Fix session cost from stdin and delegation hook subagent deadlock

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• Replace broken daily cost calculation with session cost from stdin JSON
  - Removed 445 lines of JSONL self-calculation code (9 functions, background refresh, cache files)
  - Added 5-line read of cost.total_cost_usd from stdin — accurate, zero I/O
• Harden delegation hook to prevent subagent deadlocks
  - Expanded subagent detection to check three env vars: CLAUDE_PARENT_SESSION_ID,
  CLAUDE_AGENT_ID, CLAUDE_SCRATCHPAD_DIR
  - Added redundant safety net inside main() rechecking subagent env vars
  - Normalized CLAUDE_PROJECT_DIR path resolution with Path.resolve()
• Bump version to 1.15.2 and update changelog
Diagram
flowchart LR
  A["Old: JSONL cost calculation<br/>9 functions, cache, background refresh"] -->|"Remove 445 lines"| B["New: stdin session cost<br/>5-line read from cost.total_cost_usd"]
  C["Subagent detection<br/>CLAUDE_PARENT_SESSION_ID only"] -->|"Expand to 3 env vars"| D["Enhanced bypass<br/>+ CLAUDE_AGENT_ID<br/>+ CLAUDE_SCRATCHPAD_DIR"]
  D -->|"Add safety net"| E["Redundant check in main()"]
  F["Path resolution<br/>CLAUDE_PROJECT_DIR"] -->|"Normalize with resolve()"| G["Symlink-safe path handling"]
Loading

Grey Divider

File Changes

1. hooks/PreToolUse/require_delegation.py 🐞 Bug fix +20/-5

Harden delegation hook subagent bypass with multiple signals

• Expanded subagent detection from single env var to three: CLAUDE_PARENT_SESSION_ID,
 CLAUDE_AGENT_ID, CLAUDE_SCRATCHPAD_DIR
• Added redundant safety net inside main() to recheck subagent env vars and prevent deadlocks
• Normalized CLAUDE_PROJECT_DIR path resolution using Path.resolve() to handle symlinks
 correctly
• Updated comments to explain multiple signals for subagent context detection

hooks/PreToolUse/require_delegation.py


2. scripts/statusline.py 🐞 Bug fix +13/-321

Replace cost calculation with stdin session cost read

• Replaced 319 lines of cost calculation code with 5-line read of cost.total_cost_usd from stdin
 JSON
• Removed all cost caching infrastructure: load_cost_cache(), save_cost_cache(),
 is_cache_valid(), _is_refresh_locked(), spawn_background_refresh(), fetch_costs_raw()
• Removed cache constants and lock file logic (COST_CACHE_FILE, COST_CACHE_TTL_SECONDS,
 COST_REFRESH_LOCK)
• Removed time and timedelta imports (no longer needed)
• Simplified cost display to show session cost directly from stdin with fallback to "$..."
 placeholder

scripts/statusline.py


3. .claude-plugin/marketplace.json ⚙️ Configuration changes +1/-1

Update marketplace plugin version

• Bumped version from 1.15.1 to 1.15.2

.claude-plugin/marketplace.json


View more (2)
4. .claude-plugin/plugin.json ⚙️ Configuration changes +1/-1

Update plugin version

• Bumped version from 1.15.1 to 1.15.2

.claude-plugin/plugin.json


5. CHANGELOG.md 📝 Documentation +11/-0

Document version 1.15.2 changes and fixes

• Added new section for version 1.15.2 documenting session cost stdin replacement and delegation
 hook hardening
• Listed all removed functions and constants related to cost calculation
• Documented expanded subagent detection and path resolution improvements

CHANGELOG.md


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. Top-level _is_subagent exits📘 Rule violation ⚙ Maintainability
Description
The hook runs subagent-detection logic and can sys.exit(...) at import time, bypassing the
main() entry flow. This violates the requirement to keep script logic in main() and have the
__main__ guard be sys.exit(main()) as the single entry point.
Code

hooks/PreToolUse/require_delegation.py[R42-48]

+_is_subagent = bool(
+    os.environ.get("CLAUDE_PARENT_SESSION_ID")
+    or os.environ.get("CLAUDE_AGENT_ID")
+    or os.environ.get("CLAUDE_SCRATCHPAD_DIR")
+)
+if _is_subagent:
# Subagent - allow all tools EXCEPT TeamCreate (no nested teams)
Evidence
PR Compliance ID 62793 requires primary script logic to be inside main() and invoked via
sys.exit(main()) in the entry-point guard. The modified code adds a module-level _is_subagent
block that executes before main() and terminates the process via sys.exit(...).

Rule 62793: Use sys.exit(main()) as the Python script entry point
hooks/PreToolUse/require_delegation.py[39-65]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`hooks/PreToolUse/require_delegation.py` performs subagent detection and calls `sys.exit(...)` at module import time (`_is_subagent` block). This bypasses the intended `main()`-based entry flow.
## Issue Context
Compliance requires executable scripts to route all primary logic through `main()` and use `sys.exit(main())` in the `if __name__ == "__main__":` guard, avoiding early termination during module import.
## Fix Focus Areas
- hooks/PreToolUse/require_delegation.py[39-65]
- hooks/PreToolUse/require_delegation.py[132-214]
- hooks/PreToolUse/require_delegation.py[288-295]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Union isinstance crashes🐞 Bug ≡ Correctness
Description
scripts/statusline.py uses isinstance(raw_val, int | float), which raises a TypeError at
runtime (the 2nd arg to isinstance cannot be a union), breaking statusline rendering.
Code

scripts/statusline.py[R434-438]

+    stdin_cost = input_data.get("cost", {}) if isinstance(input_data, dict) else {}
+    if isinstance(stdin_cost, dict):
+        raw_val = stdin_cost.get("total_cost_usd")
+        if isinstance(raw_val, int | float):
+            session_cost_usd = float(raw_val)
Evidence
The new session-cost parsing path calls isinstance(raw_val, int | float), which is an invalid
runtime type spec for isinstance, so the script can crash whenever stdin contains a
cost.total_cost_usd value (or even when it attempts the check).

scripts/statusline.py[432-442]
scripts/statusline.py[389-421]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scripts/statusline.py` uses `isinstance(raw_val, int | float)` which is not a valid type argument for `isinstance` at runtime and can crash the statusline.
### Issue Context
The statusline reads JSON from stdin and extracts `cost.total_cost_usd`. The type guard must not throw.
### Fix Focus Areas
- scripts/statusline.py[432-442]
### Suggested change
Replace:
- `isinstance(raw_val, int | float)`
with:
- `isinstance(raw_val, (int, float))`
(Optionally also accept numeric strings by attempting `float(raw_val)` inside a try/except if that’s expected input.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Subagent bypass disables enforcement🐞 Bug ⛨ Security
Description
require_delegation.py treats CLAUDE_SCRATCHPAD_DIR as a subagent indicator and exits early with
success; since docs describe CLAUDE_SCRATCHPAD_DIR as per-session (not subagent-only), this can
bypass delegation enforcement for top-level sessions.
Code

hooks/PreToolUse/require_delegation.py[R42-47]

+_is_subagent = bool(
+    os.environ.get("CLAUDE_PARENT_SESSION_ID")
+    or os.environ.get("CLAUDE_AGENT_ID")
+    or os.environ.get("CLAUDE_SCRATCHPAD_DIR")
+)
+if _is_subagent:
Evidence
The hook now considers CLAUDE_SCRATCHPAD_DIR sufficient to classify execution as subagent context
and calls sys.exit(0) at import time. Internal documentation lists CLAUDE_SCRATCHPAD_DIR as
“Per-session”, implying it may be present outside subagents; if so, the hook will be skipped and
tool blocking won’t occur for the main agent.

hooks/PreToolUse/require_delegation.py[39-65]
CLAUDE.md[237-252]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The delegation hook’s subagent bypass triggers when `CLAUDE_SCRATCHPAD_DIR` is set, but docs describe it as per-session, so this can inadvertently classify the top-level agent as a subagent and skip enforcement.
### Issue Context
This hook is the primary enforcement mechanism (“block tools unless /delegate was used”). The bypass runs at module import and immediately `sys.exit(0)`.
### Fix Focus Areas
- hooks/PreToolUse/require_delegation.py[39-65]
- hooks/PreToolUse/require_delegation.py[142-151]
- CLAUDE.md[237-252]
### Suggested change
- Remove `CLAUDE_SCRATCHPAD_DIR` from `_is_subagent` detection.
- If additional signals are needed beyond `CLAUDE_PARENT_SESSION_ID`, only use env vars that are confirmed subagent-only in this repo’s docs.
- Keep the existing `delegation_active` flag mechanism (already present) for Skill subagents that lack `CLAUDE_PARENT_SESSION_ID`, rather than broadening the bypass.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Zero cost shown unknown 🐞 Bug ≡ Correctness
Description
cost_str uses a truthiness check, so a valid total_cost_usd of 0 renders as $... instead of
$0.00, misreporting known zero-cost sessions as unknown.
Code

scripts/statusline.py[R440-442]

+    cost_str = (
+        f"\U0001f4b0 ${session_cost_usd:.2f}" if session_cost_usd else "\U0001f4b0 $..."
)
Evidence
session_cost_usd initializes to 0.0 and the display uses if session_cost_usd to decide between
formatting and placeholder; 0.0 is falsy, so it will always show $... when the actual cost is
exactly zero.

scripts/statusline.py[432-442]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The statusline shows `$...` when `total_cost_usd` is `0.0` because it uses a truthiness check.
### Issue Context
`0.0` is a valid known cost and should render as `$0.00`.
### Fix Focus Areas
- scripts/statusline.py[432-442]
### Suggested change
- Use a sentinel `None` for unknown cost:
- `session_cost_usd: float | None = None`
- set it only when parsing succeeds
- render with `if session_cost_usd is not None` rather than truthiness.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread hooks/PreToolUse/require_delegation.py Outdated
Comment thread scripts/statusline.py
Comment thread hooks/PreToolUse/require_delegation.py Outdated
@barkain
barkain merged commit a1424cf into main Apr 6, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant