Investigate CLI Prompt Injection on Trails · Entire
Does the little prompt injection work that the CLI has on trails? Investigate
Yes — at the CLI/hook layer it works.
What I found:
- Injection text lives in
cmd/entire/cli/lifecycle.go:A trail ties together the context for a branch. Use \entire trail` to view, create, update, or watch it.`
- It emits once per normal session on
TurnStart, gated by clone-local.git/entire/preferences.jsonhaving"trails_enabled": true. - Agents render it as:
- Claude/Codex/Gemini:
hookSpecificOutput.additionalContext - Pi/OpenCode:
{"inject_context": "..."}which their extension/plugin parses and injects.
- Claude/Codex/Gemini:
I verified:
- Targeted tests pass:
ContextInjectorTrailsEnabledForRepo- OpenCode plugin injection test
- Manual hook simulation in a temp repo with
trails_enabled:trueproduced the expected stdout payload for Claude, Codex, Gemini, Pi, and OpenCode. - Second turn emits nothing, as intended.
- With
trails_enabled:false, emits nothing and marks the session decided.
Caveats:
- I did not run real-agent E2E; that would cost tokens.
- It only works if the local trails-enabled cache exists and is true. That cache is only refreshed during
entire enable; already-enabled clones or stale false/nil caches won’t get the injection until re-enable or another cache refresh path is added.
How about in THIS repo?
For this repo/session: no, not effectively.
Findings:
- Entire is enabled:
.entire/settings.jsonhas"enabled": true. - Pi extension is installed and has the injection plumbing.
- Current Pi session state has
context_injection_decided: true. - But the local trail enablement cache is missing:
- expected:
.git/entire/preferences.json - actual: missing
- expected:
- Since the code fails closed on missing cache, the hook marks injection “decided” but emits nothing.
- I checked the current Pi transcript: no hidden
entire-contextinjected message was present.
Also: .opencode/plugins/entire.ts in this repo looks stale; it does not include the newer inject_context / experimental.chat.system.transform logic.
So: the code works generally, but this repo’s local hook state/cache means the trail prompt injection did not reach this current Pi session.
Dig deeper, does the preferences.json actually work?
Short answer: the preferences file works as a local gate, but the population/refresh path is weak — in this repo it is not populated even though trails are accessible.
What I verified:
- Current repo path resolves to:
.git/entire/preferences.json
- Currently it is missing.
settings.LoadClonePreferences()treats missing as{}→trailsEnabled == nil→ injection gate is false.- Temporarily writing:
{"trails_enabled": true}made the gate readtrue{"trails_enabled": false}made it readfalse
- Existing test passes:
TestTrailsEnabledForRepo_ReadsClonePreference
But importantly:
go run ./cmd/entire/main.go trail list --limit 1 --status anyworks in this repo and shows trails exist/access is enabled:Recent trails · 1/631
- Yet
.git/entire/preferences.jsonis absent. - So normal trail usage does not refresh/write the cache.
- It looks like only
entire enablewrites this viareportRepoEnabled(), and that path is best-effort/silent. Existing enabled clones can easily never get the cache.
Current session impact:
- Latest Pi session has
context_injection_decided: true. - Since prefs were missing at first turn, the session failed closed and won’t retry even if prefs are added later.
How can we make it more reliable then?
Only writing on entire enable is not enough; it should more "constantly" check based on certain events. Are the preferences.json git ignored?
Yes — preferences.json is safe from git, but the refresh model is too sparse.
Reliability improvements I’d make:
- Refresh on successful trail commands
- Any successful
entire trail list/show/create/update/delete/finding/watchresponse proves trails are enabled/accesssible. - So after those 2xx responses, call
saveTrailsEnabledForRepo(ctx, true).
- Any successful
- Refresh on definitive disabled responses
- On 404/403/410 from trails endpoints, save
false. - On network errors, auth errors, 5xx: do not overwrite the cache.
- On 404/403/410 from trails endpoints, save
- Add freshness metadata
- Extend clone prefs with e.g.
trails_enabled_checked_attrails_enabled_repo/ remote key
- Avoid stale true after origin changes or after trails are disabled.
- Extend clone prefs with e.g.
- Refresh opportunistically before agent turns
- On
SessionStart, if cache is missing/stale, kick off a bounded/background refresh.
- On
- Keep hook-time gate simple
TurnStartshould only read local prefs:- fresh true → inject + mark decided
- fresh false → mark decided/no inject
- unknown/stale → request refresh, don’t mark decided
The key bug is:
missing prefs currently behaves like false and is sticky per session. Better is unknown → refresh + retry later.
All right, which are the safest triggers to make sure this isn't stale ever?
If we mean “not stale ever”, triggers alone aren’t enough. The safest model is:
- Validate on use
- Before injecting, only trust a cache entry if it matches current:
- repo key:
forge/owner/repo - origin URL hash or canonical remote
- API base/core/context identity
- checked timestamp / generation
- repo key:
- If it doesn’t match: treat as unknown, not false.
- Before injecting, only trust a cache entry if it matches current:
- Refresh on agent
SessionStart- Best safe trigger for prompt injection.
- Refresh on every successful trail API command
trail list/show/create/update/delete/finding/watch- Any 2xx from repo-scoped trail endpoint → write
trails_enabled: true.
- Invalidate on identity changes
auth login/logout/use
- Use a TTL
- Even with triggers, server-side state can change without CLI knowing.
Key behavioral change:
unknown/stale must not set context_injection_decided. Current behavior makes missing prefs sticky for the whole session, which is the main bug.
Sounds good, let's make the changes.
Implemented the trail context-injection cache refresh changes.
Key changes:
- Added scoped/fresh trail enablement cache in
.git/entire/preferences.json:trails_enabledtrails_enabled_checked_attrails_enabled_repo_keytrails_enabled_api_basetrails_enabled_auth_key
TurnStartnow only reads local cache and injects only when cache is freshtrue.- Missing/stale/mismatched cache is now
unknownand does not setContextInjectionDecided.
Validation run:
go test ./cmd/entire/cli/...mise run lint
Create a trail commit and push the changes to the new branch.
Done.
- Branch:
dip/trail-context-cache-refresh - Commit:
de2087233 Gate trail injection on enablement cache - Pushed to:
origin/dip/trail-context-cache-refresh - PR URL: https://github.com/entireio/cli/pull/new/dip/trail-context-cache-refresh
Validation run:
mise run checkmise run lint- Working tree is clean.