Skip to content

Commit fbef453

Browse files
committed
refactor: remove CLAUDE_ENV_FILE from ClaudeTool and ClaudeHook scopes
1 parent e1b5837 commit fbef453

18 files changed

Lines changed: 232 additions & 310 deletions
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
{"status": "open"}
1+
{
2+
"status": "closed",
3+
"resolution": "implemented"
4+
}

.cat/issues/v2/v2.1/remove-env-file-from-scopes/plan.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,127 @@ is not set outside of SessionStart context.
2525
- [ ] Tests passing — `mvn -f client/pom.xml verify -e` exits 0
2626
- [ ] No regressions in existing functionality
2727
- [ ] E2E: Invoke a CLI tool (e.g., `get-status-output`) and confirm it no longer requires `CLAUDE_ENV_FILE` to be set
28+
29+
## Research Findings
30+
31+
### Current Architecture
32+
33+
The `getEnvFile()` method exists in two parallel interface hierarchies:
34+
- `ClaudeTool` (for CLI tools) → `AbstractClaudeTool` (stores `envFile` field) → `MainClaudeTool` (reads env var at
35+
construction)
36+
- `ClaudeHook` (for hook handlers) → `AbstractClaudeHook` (declares abstract) → `MainClaudeHook` (reads env var
37+
on-demand)
38+
39+
Neither `JvmScope` (the base interface) defines `getEnvFile()` — it's specific to `ClaudeTool` and `ClaudeHook`.
40+
41+
### Single Real Consumer
42+
43+
Only `InjectEnv.java` (line 68) calls `scope.getEnvFile()`. It uses the path to write environment variable export
44+
statements for subsequent Bash tool invocations. `InjectEnv` receives a `ClaudeTool` scope, so it can simply call
45+
`System.getenv("CLAUDE_ENV_FILE")` directly instead of going through the scope.
46+
47+
### Files to Modify
48+
49+
| File | Change |
50+
|------|--------|
51+
| `client/src/main/java/io/github/cowwoc/cat/hooks/ClaudeTool.java` | Remove `getEnvFile()` method declaration |
52+
| `client/src/main/java/io/github/cowwoc/cat/hooks/ClaudeHook.java` | Remove `getEnvFile()` method declaration |
53+
| `client/src/main/java/io/github/cowwoc/cat/hooks/AbstractClaudeTool.java` | Remove `envFile` field, constructor param, `getEnvFile()` impl |
54+
| `client/src/main/java/io/github/cowwoc/cat/hooks/AbstractClaudeHook.java` | Remove abstract `getEnvFile()` declaration |
55+
| `client/src/main/java/io/github/cowwoc/cat/hooks/MainClaudeTool.java` | Remove `CLAUDE_ENV_FILE` from constructor |
56+
| `client/src/main/java/io/github/cowwoc/cat/hooks/MainClaudeHook.java` | Remove `getEnvFile()` implementation |
57+
| `client/src/main/java/io/github/cowwoc/cat/hooks/session/InjectEnv.java` | Read `CLAUDE_ENV_FILE` via `System.getenv()` directly |
58+
| `client/src/test/java/io/github/cowwoc/cat/hooks/test/TestClaudeTool.java` | Remove envFile constructor params and `withEnvFile()` factory |
59+
| `client/src/test/java/io/github/cowwoc/cat/hooks/test/TestClaudeHook.java` | Remove `getEnvFile()` implementation |
60+
| `client/src/test/java/io/github/cowwoc/cat/hooks/test/TestClaudeToolTest.java` | Remove `getEnvFile()` test assertion |
61+
| `client/src/test/java/io/github/cowwoc/cat/hooks/test/TestClaudeHookTest.java` | Remove `getEnvFile()` test assertions |
62+
| `client/src/test/java/io/github/cowwoc/cat/hooks/test/EnforceJvmScopeEnvAccessTest.java` | Add `InjectEnv.java` to whitelist |
63+
64+
## Sub-Agent Waves
65+
66+
### Wave 1
67+
68+
Each step below is a discrete edit. Read each file before editing. Commit type: `refactor:`.
69+
70+
**Step 1: Remove `getEnvFile()` from `ClaudeTool` interface**
71+
- File: `client/src/main/java/io/github/cowwoc/cat/hooks/ClaudeTool.java`
72+
- Remove the `getEnvFile()` method declaration and its Javadoc comment block
73+
74+
**Step 2: Remove `getEnvFile()` from `ClaudeHook` interface**
75+
- File: `client/src/main/java/io/github/cowwoc/cat/hooks/ClaudeHook.java`
76+
- Remove the `getEnvFile()` method declaration and its Javadoc comment block
77+
78+
**Step 3: Remove `envFile` from `AbstractClaudeTool`**
79+
- File: `client/src/main/java/io/github/cowwoc/cat/hooks/AbstractClaudeTool.java`
80+
- Remove the `private final Path envFile;` field
81+
- Remove the `envFile` parameter from the protected constructor and the `this.envFile = envFile;` assignment
82+
- Remove the entire `getEnvFile()` method implementation
83+
- Remove the `@Override` annotation above `getEnvFile()` if present
84+
- Keep all other fields and methods intact
85+
86+
**Step 4: Remove abstract `getEnvFile()` from `AbstractClaudeHook`**
87+
- File: `client/src/main/java/io/github/cowwoc/cat/hooks/AbstractClaudeHook.java`
88+
- Remove the `@Override public abstract Path getEnvFile();` declaration
89+
90+
**Step 5: Remove `CLAUDE_ENV_FILE` from `MainClaudeTool` constructor**
91+
- File: `client/src/main/java/io/github/cowwoc/cat/hooks/MainClaudeTool.java`
92+
- In the constructor, remove the `Path.of(getEnvVar("CLAUDE_ENV_FILE"))` argument from the `super()` call
93+
- The super call should become:
94+
`super(getEnvVar("CLAUDE_SESSION_ID"), Path.of(getEnvVar("CLAUDE_PROJECT_DIR")), Path.of(getEnvVar("CLAUDE_PLUGIN_ROOT")))`
95+
96+
**Step 6: Remove `getEnvFile()` from `MainClaudeHook`**
97+
- File: `client/src/main/java/io/github/cowwoc/cat/hooks/MainClaudeHook.java`
98+
- Remove the entire `getEnvFile()` method (the `@Override` block that reads `System.getenv("CLAUDE_ENV_FILE")`)
99+
100+
**Step 7: Update `InjectEnv` to read `CLAUDE_ENV_FILE` directly**
101+
- File: `client/src/main/java/io/github/cowwoc/cat/hooks/session/InjectEnv.java`
102+
- At line 68 (or wherever `Path envPath = scope.getEnvFile();` appears), replace with:
103+
```java
104+
String envFileValue = System.getenv("CLAUDE_ENV_FILE");
105+
if (envFileValue == null || envFileValue.isBlank())
106+
throw new AssertionError("CLAUDE_ENV_FILE is not set");
107+
Path envPath = Path.of(envFileValue);
108+
```
109+
- Update imports if needed (should already have `java.nio.file.Path`)
110+
111+
**Step 8: Update `TestClaudeTool`**
112+
- File: `client/src/test/java/io/github/cowwoc/cat/hooks/test/TestClaudeTool.java`
113+
- Remove the `envFile` parameter from ALL constructors (there are multiple — check each one)
114+
- Remove the `withEnvFile()` factory method (around line 194-198)
115+
- Update `super()` calls to no longer pass `envFile`
116+
- Remove any constructor that defaults to `claudeProjectPath.resolve(".env")` — remove that default
117+
118+
**Step 9: Update `TestClaudeHook`**
119+
- File: `client/src/test/java/io/github/cowwoc/cat/hooks/test/TestClaudeHook.java`
120+
- Remove the `getEnvFile()` implementation method
121+
122+
**Step 10: Update `TestClaudeToolTest`**
123+
- File: `client/src/test/java/io/github/cowwoc/cat/hooks/test/TestClaudeToolTest.java`
124+
- Remove any test assertion that calls `getEnvFile()` (around line 276)
125+
126+
**Step 11: Update `TestClaudeHookTest`**
127+
- File: `client/src/test/java/io/github/cowwoc/cat/hooks/test/TestClaudeHookTest.java`
128+
- Remove test assertions that call `getEnvFile()` (around lines 42 and 184)
129+
- If entire test methods only test `getEnvFile()`, remove the whole test method
130+
131+
**Step 12: Update `EnforceJvmScopeEnvAccessTest` whitelist**
132+
- File: `client/src/test/java/io/github/cowwoc/cat/hooks/test/EnforceJvmScopeEnvAccessTest.java`
133+
- Add `io/github/cowwoc/cat/hooks/session/InjectEnv.java` to the whitelist set (around lines 64-73)
134+
- This authorizes `InjectEnv` to call `System.getenv()` directly
135+
- If `MainClaudeHook.java` no longer calls `System.getenv("CLAUDE_ENV_FILE")`, verify whether it still
136+
needs to be in the whitelist (it may call `System.getenv()` for other variables — check before removing)
137+
138+
**Step 13: Remove unused imports**
139+
- After all changes, check each modified file for unused imports (especially `java.nio.file.Path` in files
140+
that no longer reference `Path`)
141+
142+
**Step 14: Build and test**
143+
- Run `mvn -f client/pom.xml verify -e` and ensure all tests pass
144+
- Fix any compilation errors from the changes above
145+
146+
**Step 15: Update `index.json`**
147+
- Update `.cat/issues/v2/v2.1/remove-env-file-from-scopes/index.json` to set status to `closed`
148+
149+
**Step 16: Commit**
150+
- Stage all changed files
151+
- Commit with message: `refactor: remove CLAUDE_ENV_FILE from ClaudeTool and ClaudeHook scopes`

client/src/main/java/io/github/cowwoc/cat/hooks/AbstractClaudeHook.java

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -175,19 +175,6 @@ private static String validateAgentId(JsonNode data)
175175
return value;
176176
}
177177

178-
/**
179-
* Returns the path to the Claude environment file.
180-
* <p>
181-
* Subclasses must implement this to provide the env file path from {@code CLAUDE_ENV_FILE}
182-
* or an injected value.
183-
*
184-
* @return the path to the env file
185-
* @throws AssertionError if {@code CLAUDE_ENV_FILE} is not set in the environment
186-
* @throws IllegalStateException if this scope is closed
187-
*/
188-
@Override
189-
public abstract Path getEnvFile();
190-
191178
@Override
192179
public Path getProjectPath()
193180
{

client/src/main/java/io/github/cowwoc/cat/hooks/AbstractClaudeTool.java

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,25 @@ public abstract class AbstractClaudeTool extends AbstractJvmScope implements Cla
2525
private final String sessionId;
2626
private final Path projectPath;
2727
private final Path pluginRoot;
28-
private final Path envFile;
2928

3029
/**
3130
* Creates a new abstract Claude tool scope with the given environment values.
3231
*
3332
* @param sessionId the Claude session ID
3433
* @param projectPath the project's root directory path (must be absolute)
3534
* @param pluginRoot the Claude plugin root directory path (must be absolute)
36-
* @param envFile the path to the Claude environment file
3735
* @throws IllegalArgumentException if {@code sessionId} is blank, or if {@code projectPath} or
3836
* {@code pluginRoot} are not absolute paths
39-
* @throws NullPointerException if {@code projectPath}, {@code pluginRoot}, or {@code envFile} are null
37+
* @throws NullPointerException if {@code projectPath} or {@code pluginRoot} are null
4038
*/
41-
protected AbstractClaudeTool(String sessionId, Path projectPath, Path pluginRoot, Path envFile)
39+
protected AbstractClaudeTool(String sessionId, Path projectPath, Path pluginRoot)
4240
{
4341
requireThat(sessionId, "sessionId").isNotBlank();
4442
requireThat(projectPath, "projectPath").isNotNull().isAbsolute();
4543
requireThat(pluginRoot, "pluginRoot").isNotNull().isAbsolute();
46-
requireThat(envFile, "envFile").isNotNull();
4744
this.sessionId = sessionId;
4845
this.projectPath = projectPath;
4946
this.pluginRoot = pluginRoot;
50-
this.envFile = envFile;
5147
}
5248

5349
@Override
@@ -70,11 +66,4 @@ public Path getPluginRoot()
7066
ensureOpen();
7167
return pluginRoot;
7268
}
73-
74-
@Override
75-
public Path getEnvFile()
76-
{
77-
ensureOpen();
78-
return envFile;
79-
}
8069
}

client/src/main/java/io/github/cowwoc/cat/hooks/AotTraining.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public static void main(String[] args) throws Exception
8383
new PreWriteHook(scope).run(scope);
8484
new PreIssueHook(scope).run(scope);
8585
new SessionEndHook(scope).run(scope);
86-
new SessionStartHook(scope).run(scope);
86+
new SessionStartHook(scope, Path.of("/tmp/aot-training-env")).run(scope);
8787
new SubagentStartHook(scope).run(scope);
8888

8989
// Skill handlers - construct to load class graphs.

client/src/main/java/io/github/cowwoc/cat/hooks/ClaudeHook.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import tools.jackson.databind.json.JsonMapper;
1111
import tools.jackson.databind.node.ObjectNode;
1212

13-
import java.nio.file.Path;
14-
1513
/**
1614
* A {@link JvmScope} for hook handler processes that combines the Claude session environment
1715
* (project path, plugin root, config dir), hook input data, and hook output building in a single
@@ -30,15 +28,6 @@ public interface ClaudeHook extends JvmScope
3028
*/
3129
String getSessionId();
3230

33-
/**
34-
* Returns the path to the Claude environment file.
35-
*
36-
* @return the path to the env file
37-
* @throws AssertionError if {@code CLAUDE_ENV_FILE} is not set in the environment
38-
* @throws IllegalStateException if this scope is closed
39-
*/
40-
Path getEnvFile();
41-
4231
// Hook input methods
4332

4433
/**

client/src/main/java/io/github/cowwoc/cat/hooks/ClaudeTool.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,4 @@ public interface ClaudeTool extends JvmScope
4545
*/
4646
@Override
4747
Path getPluginRoot();
48-
49-
/**
50-
* Returns the path to the Claude environment file.
51-
*
52-
* @return the environment file path
53-
* @throws AssertionError if {@code CLAUDE_ENV_FILE} is not set in the environment
54-
* @throws IllegalStateException if this scope is closed
55-
*/
56-
Path getEnvFile();
5748
}

client/src/main/java/io/github/cowwoc/cat/hooks/MainClaudeHook.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,6 @@ private static Path readConfigDir()
103103
return Path.of(System.getProperty("user.home"), ".claude");
104104
}
105105

106-
@Override
107-
public Path getEnvFile()
108-
{
109-
ensureOpen();
110-
String value = System.getenv("CLAUDE_ENV_FILE");
111-
if (value == null || value.isBlank())
112-
throw new AssertionError("CLAUDE_ENV_FILE is not set");
113-
return Path.of(value);
114-
}
115-
116106
@Override
117107
public Path getWorkDir()
118108
{

client/src/main/java/io/github/cowwoc/cat/hooks/MainClaudeTool.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
* Production implementation of {@link JvmScope} for CLI tool processes.
1717
* <p>
1818
* Reads session environment values ({@code CLAUDE_SESSION_ID}, {@code CLAUDE_PROJECT_DIR},
19-
* {@code CLAUDE_PLUGIN_ROOT}, {@code CLAUDE_ENV_FILE}) from {@code System.getenv()} at
20-
* construction time and passes them to {@link AbstractClaudeTool}.
19+
* {@code CLAUDE_PLUGIN_ROOT}) from {@code System.getenv()} at construction time and passes
20+
* them to {@link AbstractClaudeTool}.
2121
* <p>
2222
* <b>Thread Safety:</b> This class is thread-safe.
2323
*/
@@ -34,7 +34,7 @@ public final class MainClaudeTool extends AbstractClaudeTool
3434
/**
3535
* Creates a new production Claude tool scope.
3636
* <p>
37-
* Reads the four required environment variables from {@code System.getenv()} and fails
37+
* Reads the three required environment variables from {@code System.getenv()} and fails
3838
* immediately with {@link AssertionError} if any are unset or blank.
3939
*
4040
* @throws AssertionError if any required environment variable is not set
@@ -43,8 +43,7 @@ public MainClaudeTool()
4343
{
4444
super(getEnvVar("CLAUDE_SESSION_ID"),
4545
Path.of(getEnvVar("CLAUDE_PROJECT_DIR")),
46-
Path.of(getEnvVar("CLAUDE_PLUGIN_ROOT")),
47-
Path.of(getEnvVar("CLAUDE_ENV_FILE")));
46+
Path.of(getEnvVar("CLAUDE_PLUGIN_ROOT")));
4847
}
4948

5049
/**

client/src/main/java/io/github/cowwoc/cat/hooks/MainJvmScope.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
* <p>
2424
* This scope is appropriate for CLI tools like {@code GetSkill} that are invoked by the skill
2525
* preprocessor before a Claude session is established and therefore do not have access to
26-
* session-specific variables ({@code CLAUDE_SESSION_ID}, {@code CLAUDE_ENV_FILE}).
26+
* session-specific variables ({@code CLAUDE_SESSION_ID}).
2727
* <p>
2828
* <b>Thread Safety:</b> This class is thread-safe.
2929
*/

0 commit comments

Comments
 (0)