Skip to content

Commit b1d1e18

Browse files
Shahinyanmclaude
andcommitted
fix(classifier): drop --bare, gate hook recursion via TJ_IN_CLASSIFIER (v0.2.9)
claude --bare ignores ~/.claude/.credentials.json and demands ANTHROPIC_API_KEY. Subscription-auth users (Pro/Max — most of the audience) hit "Not logged in" on every classification, so the journal stayed empty in production. claude-memory-0kk. We previously needed --bare to stop the classifier path from spawning a `claude -p` that re-reads settings.json and re-fires our hooks recursively. Replace that mechanism with an explicit sentinel env var: - ClaudeCliClassifier.classify() drops --bare from argv and sets TJ_IN_CLASSIFIER=1 on the spawned process. - IngestHook handler returns Ok(()) immediately when it sees TJ_IN_CLASSIFIER in env. Recursion broken without touching auth. One regression test (ingest_hook_short_circuits_when_in_classifier_ env_set) plus the existing 40 ingest-hook tests stay green. Trade-off: without --bare the inner claude loads CLAUDE.md / skills on the first call per 5-minute prompt-cache window (one-time ~60K cache_creation tokens; subsequent calls hit cache and stay cheap). The classifier prompt explicitly demands JSON-only output, so model compliance survives the larger system prompt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8b4602e commit b1d1e18

11 files changed

Lines changed: 117 additions & 11 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@
66
},
77
"metadata": {
88
"description": "Task Journal — append-only reasoning chain memory for AI-coding tasks",
9-
"version": "0.2.8"
9+
"version": "0.2.9"
1010
},
1111
"plugins": [
1212
{
1313
"name": "task-journal",
1414
"source": "./plugin",
1515
"description": "Append-only journal of AI-coding task reasoning chains. Captures hypotheses, decisions, rejections, evidence — renders compact resume packs so an agent can pick up a 2-week-old task with full context.",
16-
"version": "0.2.8",
16+
"version": "0.2.9",
1717
"author": {
1818
"name": "Digital-Threads"
1919
},

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.2.9] - 2026-05-07
11+
12+
Critical fix: classifier path now works for users on Claude Pro/Max
13+
subscription (the majority of Claude Code users). v0.2.8 still
14+
shipped `--bare` to the inner `claude -p` invocation; that flag
15+
silently bypasses `~/.claude/.credentials.json` and demands
16+
`ANTHROPIC_API_KEY`. With only a subscription, every classification
17+
returned "Not logged in".
18+
19+
### Fixed
20+
- `ClaudeCliClassifier` no longer passes `--bare`. Hook recursion
21+
(the original reason for `--bare`) is now broken via an explicit
22+
env-var sentinel: the classifier sets `TJ_IN_CLASSIFIER=1` on the
23+
child process, and `ingest-hook` returns immediately when it sees
24+
that env. One regression test (`ingest_hook_short_circuits_when_in_
25+
classifier_env_set`) covers the guard. Closes claude-memory-0kk.
26+
27+
### Notes
28+
- Without `--bare`, the inner `claude -p` loads the user's
29+
`CLAUDE.md`, skills, and hooks. That increases the prompt-cache
30+
cost the first time per 5-minute window. The classifier prompt is
31+
explicit about the JSON-only output contract, so model compliance
32+
is preserved; subsequent calls within the cache TTL hit the
33+
prompt cache and stay cheap.
34+
1035
## [0.2.8] - 2026-05-07
1136

1237
Critical fix: hooks now actually carry content end-to-end. Without

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ members = [
77
]
88

99
[workspace.package]
10-
version = "0.2.8"
10+
version = "0.2.9"
1111
edition = "2021"
1212
rust-version = "1.88"
1313
license = "MIT"

crates/tj-cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ name = "task-journal"
1616
path = "src/main.rs"
1717

1818
[dependencies]
19-
tj-core = { package = "task-journal-core", version = "0.2.8", path = "../tj-core" }
19+
tj-core = { package = "task-journal-core", version = "0.2.9", path = "../tj-core" }
2020
anyhow = { workspace = true }
2121
clap = { workspace = true }
2222
tracing = { workspace = true }

crates/tj-cli/src/main.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,19 @@ fn main() -> Result<()> {
11861186
mock_task_id,
11871187
mock_confidence,
11881188
} => {
1189+
// Recursion guard. The classifier spawns `claude -p` to do
1190+
// the actual work; that nested claude invocation re-reads
1191+
// ~/.claude/settings.json and would re-fire our hooks,
1192+
// recursively calling ingest-hook → classifier → claude → …
1193+
// Until v0.2.8 we relied on `--bare` to suppress the hooks
1194+
// on the inner invocation, but --bare doesn't work with
1195+
// subscription auth (claude-memory-0kk), so the classifier
1196+
// now sets TJ_IN_CLASSIFIER=1 in the child env and we bail
1197+
// here when we see it.
1198+
if std::env::var("TJ_IN_CLASSIFIER").is_ok() {
1199+
return Ok(());
1200+
}
1201+
11891202
// Resolve (kind, text) source: explicit args win; otherwise
11901203
// read the Claude Code hook payload from stdin. The earlier
11911204
// settings.json template interpolated `$CLAUDE_HOOK_NAME` /

crates/tj-cli/tests/cli.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,67 @@ fn ingest_hook_session_start_with_no_open_tasks_emits_no_context() {
12361236
);
12371237
}
12381238

1239+
#[test]
1240+
fn ingest_hook_short_circuits_when_in_classifier_env_set() {
1241+
// Recursion guard: classifier sets TJ_IN_CLASSIFIER=1 before
1242+
// spawning claude. The nested claude re-fires our hooks; without
1243+
// this guard, ingest-hook would re-enter the classifier path
1244+
// ad infinitum. With the guard, it returns silently and no event
1245+
// is written.
1246+
let dir = assert_fs::TempDir::new().unwrap();
1247+
let task_id = String::from_utf8(
1248+
Command::cargo_bin("task-journal")
1249+
.unwrap()
1250+
.env("XDG_DATA_HOME", dir.path())
1251+
.args(["create", "Recursion guard host"])
1252+
.assert()
1253+
.success()
1254+
.get_output()
1255+
.stdout
1256+
.clone(),
1257+
)
1258+
.unwrap()
1259+
.trim()
1260+
.to_string();
1261+
1262+
Command::cargo_bin("task-journal")
1263+
.unwrap()
1264+
.env("XDG_DATA_HOME", dir.path())
1265+
.env("TJ_IN_CLASSIFIER", "1")
1266+
.args([
1267+
"ingest-hook",
1268+
"--kind",
1269+
"UserPromptSubmit",
1270+
"--text",
1271+
"should not be ingested",
1272+
"--mock-event-type",
1273+
"decision",
1274+
"--mock-task-id",
1275+
&task_id,
1276+
"--mock-confidence",
1277+
"0.99",
1278+
])
1279+
.assert()
1280+
.success();
1281+
1282+
// The pack must NOT contain the hook text — guard kicked in
1283+
// before the mock branch could write.
1284+
let out = Command::cargo_bin("task-journal")
1285+
.unwrap()
1286+
.env("XDG_DATA_HOME", dir.path())
1287+
.args(["pack", &task_id, "--mode", "full"])
1288+
.assert()
1289+
.success()
1290+
.get_output()
1291+
.stdout
1292+
.clone();
1293+
let body = String::from_utf8(out).unwrap();
1294+
assert!(
1295+
!body.contains("should not be ingested"),
1296+
"TJ_IN_CLASSIFIER must short-circuit before any write: {body}"
1297+
);
1298+
}
1299+
12391300
#[test]
12401301
fn ingest_hook_reads_user_prompt_submit_payload_from_stdin() {
12411302
// Real Claude Code passes hook input as JSON over stdin, NOT via env

crates/tj-core/src/classifier/cli.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ impl Classifier for ClaudeCliClassifier {
5757
.ok_or_else(|| anyhow!("TJ_CLASSIFIER_CLI is empty"))?;
5858
let base_args: Vec<&str> = parts.collect();
5959

60+
// `--bare` would skip hooks/CLAUDE.md/skills (cheap + non-recursive),
61+
// but it ALSO bypasses the subscription credentials file and only
62+
// works with ANTHROPIC_API_KEY — see claude-memory-0kk. Most users
63+
// pay through Claude Pro/Max, so we drop --bare and gate recursion
64+
// explicitly with the TJ_IN_CLASSIFIER env var: ingest-hook checks
65+
// it on entry and short-circuits, breaking the loop without
66+
// touching auth.
6067
let output = std::process::Command::new(program)
6168
.args(&base_args)
6269
.args([
@@ -65,9 +72,9 @@ impl Classifier for ClaudeCliClassifier {
6572
&self.model,
6673
"--output-format",
6774
"json",
68-
"--bare", // skip hooks/skills/CLAUDE.md to avoid recursion + speed up
6975
&prompt,
7076
])
77+
.env("TJ_IN_CLASSIFIER", "1")
7178
.output()
7279
.with_context(|| format!("spawn `{}` for classification", self.command))?;
7380

crates/tj-mcp/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ name = "task-journal-mcp"
1616
path = "src/main.rs"
1717

1818
[dependencies]
19-
tj-core = { package = "task-journal-core", version = "0.2.8", path = "../tj-core" }
19+
tj-core = { package = "task-journal-core", version = "0.2.9", path = "../tj-core" }
2020
anyhow = { workspace = true }
2121
tokio = { workspace = true }
2222
tracing = { workspace = true }

plugin/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "task-journal",
3-
"version": "0.2.8",
3+
"version": "0.2.9",
44
"description": "Append-only journal of AI-coding task reasoning chains: hypotheses, decisions, rejections, evidence. Renders compact resume packs so an agent can pick up a 2-week-old task with full context.",
55
"author": {
66
"name": "Mher Shahinyan"

0 commit comments

Comments
 (0)