You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
**Why:** Creating a new scope in the catch block reads environment variables again, adds latency, and opens two resources
1544
+
sequentially when one would suffice. The single outer scope is still open during the catch block, so it can be used
1545
+
directly for error reporting.
1546
+
1397
1547
## Warnings Suppression
1398
1548
1399
1549
### @SuppressWarnings("unchecked")
@@ -1432,20 +1582,36 @@ depending on the execution context. Never call `System.getenv()` directly outsid
1432
1582
1433
1583
| Context | Correct API |
1434
1584
|---------|-------------|
1435
-
| CLI commands (`main()` methods) |`new ClaudeEnv().getSessionId()` — `getSessionId()` is an instance method |
1585
+
| Session CLI commands (`main()` methods) — have `CLAUDE_SESSION_ID` and `CLAUDE_ENV_FILE`|`scope.getSessionId()` via `ClaudeTool`|
1586
+
| Infrastructure CLI commands (`main()` methods) — invoked outside a session (e.g., by skill preprocessor) |`scope.getPluginRoot()` etc. via `JvmScope`|
1436
1587
| Hook handlers |`HookInput.getSessionId()`|
1437
1588
| Skill directive variable substitution |`System.getenv(name)` (whitelisted; see below) |
1438
1589
1439
1590
**Why:** Hook handlers receive session-specific values from the `HookInput` JSON payload, not from environment
1440
-
variables. Reading environment variables in hook handlers bypasses this contract. CLI commands that run outside of
1441
-
hook invocation (e.g., `GetSkill`, `WorkPrepare`) use `ClaudeEnv` which wraps `System.getenv()` with validation.
1591
+
variables. Reading environment variables in hook handlers bypasses this contract. Session CLI commands use
1592
+
`MainClaudeTool` (a `ClaudeTool` implementation) which reads all env vars at startup. Infrastructure CLI commands
1593
+
(e.g., `GetSkill`) use `MainJvmScope` which reads only infrastructure path vars and does not require
1594
+
`CLAUDE_SESSION_ID` or `CLAUDE_ENV_FILE`.
1442
1595
1443
1596
```java
1444
-
// Good - CLI main() method reads session ID via ClaudeEnv (instance method)
1597
+
// Good - session CLI main() method reads session ID via scope
1445
1598
publicstaticvoid main(String[] args)
1446
1599
{
1447
-
String sessionId =newClaudeEnv().getSessionId();
1448
-
// ...
1600
+
try (ClaudeTool scope =newMainClaudeTool())
1601
+
{
1602
+
String sessionId = scope.getSessionId();
1603
+
// ...
1604
+
}
1605
+
}
1606
+
1607
+
// Good - infrastructure CLI main() method uses MainJvmScope (no session vars required)
1608
+
publicstaticvoid main(String[] args)
1609
+
{
1610
+
try (JvmScope scope =newMainJvmScope())
1611
+
{
1612
+
Path pluginRoot = scope.getPluginRoot();
1613
+
// ...
1614
+
}
1449
1615
}
1450
1616
1451
1617
// Bad - CLI main() method reads session ID via System.getenv()
@@ -1471,22 +1637,37 @@ public Result handle(HookInput input)
1471
1637
```
1472
1638
1473
1639
`EnforceJvmScopeEnvAccessTest` enforces this convention by scanning all Java source files and failing the build if
1474
-
`System.getenv(` appears outside of `MainJvmScope.java`, `ClaudeEnv.java`, `GetSkill.java`, and `TerminalType.java`.
1475
-
1476
-
The four whitelisted files each have a specific reason for direct env var access:
`CLAUDE_CONFIG_DIR`, `TZ`) available in both hook and CLI contexts
1479
-
-`ClaudeEnv.java` — the designated wrapper for session-specific variables (`CLAUDE_SESSION_ID`,
1480
-
`CLAUDE_ENV_FILE`); all other code must use `ClaudeEnv` to access these
1640
+
`System.getenv(` appears outside of `MainClaudeTool.java`, `MainJvmScope.java`, `MainClaudeHook.java`,
1641
+
`GetSkill.java`, and `TerminalType.java`.
1642
+
1643
+
The five whitelisted files each have a specific reason for direct env var access:
1644
+
-`MainClaudeTool.java` — reads all Claude env vars (`CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`,
1645
+
`CLAUDE_SESSION_ID`, `CLAUDE_ENV_FILE`, `TZ`) at startup and stores them as fields; for CLI tools that run
1646
+
as part of a Claude session
1647
+
-`MainJvmScope.java` — reads only infrastructure path vars (`CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`,
1648
+
`CLAUDE_CONFIG_DIR`, `TZ`); for CLI tools that run without a Claude session (e.g., `GetSkill` which is
1649
+
invoked by the skill preprocessor before a session is established)
1650
+
-`MainClaudeHook.java` — reads infrastructure path vars (`CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`,
1651
+
`CLAUDE_CONFIG_DIR`, `CLAUDE_ENV_FILE`, `TZ`) from the environment; the production hook scope implementation
1652
+
used in hook handler `main()` methods
1481
1653
-`GetSkill.java` — expands env var references in skill directive templates; requires direct access to
1482
1654
substitute variable values
1483
1655
-`TerminalType.java` — detects terminal type from standard terminal env vars (`TERM`, `TERM_PROGRAM`)
1484
1656
1657
+
**Scope implementations:**
1658
+
-`MainClaudeTool` — production use for session CLI tools (in `main()` methods that require `CLAUDE_SESSION_ID`
1659
+
and `CLAUDE_ENV_FILE`), reads all session environment configuration
1660
+
-`MainJvmScope` — production use for infrastructure CLI tools (in `main()` methods that do NOT require session
1661
+
vars, e.g., `GetSkill`), reads only infrastructure vars
1662
+
-`MainClaudeHook` — production use for hook handler `main()` methods, reads infrastructure path vars and hook
1663
+
JSON from stdin
1664
+
-`TestClaudeTool` — test use, accepts injectable paths: `new TestClaudeTool(tempDir, tempDir)`
1665
+
1485
1666
```cat-rules
1486
1667
- pattern: "System\\.getenv\\("
1487
1668
files: "*.java"
1488
1669
severity: high
1489
-
message: "Use ClaudeEnv (CLI commands) or HookInput (hook handlers) instead of System.getenv(). Direct access is only permitted in the four whitelisted files. See .claude/rules/java.md § Environment Variable Access."
1670
+
message: "Use scope.getSessionId() (session CLI commands) or scope.getPluginRoot() via MainJvmScope (infrastructure CLI commands) or HookInput (hook handlers) instead of System.getenv(). Direct access is only permitted in the five whitelisted files. See .claude/rules/java.md § Environment Variable Access."
1490
1671
```
1491
1672
1492
1673
## Exception Handling
@@ -1500,10 +1681,7 @@ state is queryable, and the caller could have checked before calling.
1500
1681
1501
1682
```java
1502
1683
// Good - AssertionError for environment invariant (caller cannot prevent or query)
1503
-
// Use ClaudeEnv to safely read session configuration
1504
-
String sessionId =newClaudeEnv().getSessionId();
1505
-
if (sessionId.isBlank())
1506
-
thrownewAssertionError("CLAUDE_SESSION_ID is not set");
1684
+
String sessionId = scope.getSessionId(); // throws AssertionError if env var not set
1507
1685
1508
1686
// Good - IllegalStateException for preventable state violation (caller can query)
1509
1687
publicvoid stop()
@@ -1691,8 +1869,8 @@ these Java-specific constraints:
1691
1869
4.**No shared mutable state** - each test must be fully self-contained
1692
1870
5.**No TestBase classes** - each test method must inline its own setup. This boilerplate is intentional and preferred
1693
1871
over shared helpers or inheritance.
1694
-
6.**Use `TestJvmScope`, not `MainJvmScope`** - tests must never use `MainJvmScope` because it reads environment
1695
-
variables that may not be set in test contexts. Use `TestJvmScope(tempDir, tempDir)` with injectable paths instead.
1872
+
6.**Use `TestClaudeTool`, not `MainClaudeTool`** - tests must never use `MainClaudeTool` because it reads environment
1873
+
variables that may not be set in test contexts. Use `TestClaudeTool(tempDir, tempDir)` with injectable paths instead.
1696
1874
7.**Never use scope-provided objects after closing the scope** - objects returned by `JvmScope` (e.g., `JsonMapper`,
1697
1875
`DisplayUtils`) must not be used after the scope is closed. Keep the scope open for the entire duration of the test.
1698
1876
Do not create helper methods like `getTestMapper()` that open a scope, extract an object, and close the scope.
@@ -1706,12 +1884,12 @@ these Java-specific constraints:
1706
1884
missing file check), passing `"."` is acceptable since no git command actually runs.
0 commit comments