Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changes/533.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**Incognito chats on Claude Code no longer give away your Claude account's email.** Claude Code tells Claude the email
you signed in with. In an incognito chat that doesn't use your memory, Sentient now tells Claude to ignore it and takes
it out of replies and of anything Claude asks Sentient to do, such as a helper's goal. Claude Code also no longer reads
your CLAUDE.md files, its own memory or git details for Sentient's chats.
2 changes: 1 addition & 1 deletion docs/PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ service name `sentient`.
| Use a **local model** (Ollama, LM Studio) | Nothing. Prompts stay on your machine. |
| Switch the model menu to **Local only** | Nothing goes to any AI model outside this computer, whatever else is set up: Sentient refuses a cloud model before sending anything, including backup models, a chat's own model, tasks, memory search and voice. A local model only counts when its address is this computer, and Ollama's cloud models count as cloud. Connected apps and web tools still reach the internet when you use them. |
| Use a **cloud model** | The conversation, relevant memories and tool results for each request go to that provider (for example Anthropic or OpenAI) under their terms. |
| Turn on **Claude through your Claude Code** (experimental) | Sentient starts the Claude Code program already on your computer for each chat reply, and Claude Code sends the conversation, relevant memories and tool results to Anthropic under your own Claude Code login and Anthropic's terms. Sentient never reads, copies or keeps your Claude login or anything in your `.claude` folder, leaves API keys and other sign-in settings out so Claude Code always uses the plan you signed in to, and Claude Code's own tools stay off: anything it wants done goes through Sentient's tools and approvals. Only your chats use it, never background work. Off until you turn it on. |
| Turn on **Claude through your Claude Code** (experimental) | Sentient starts the Claude Code program already on your computer for each chat reply, and Claude Code sends the conversation, relevant memories and tool results to Anthropic under your own Claude Code login and Anthropic's terms. Sentient never reads, copies or keeps your Claude login or anything in your `.claude` folder, leaves API keys and other sign-in settings out so Claude Code always uses the plan you signed in to, and Claude Code's own tools stay off: anything it wants done goes through Sentient's tools and approvals. Only your chats use it, never background work. Off until you turn it on. Claude Code always tells Claude the email of the Claude account you signed in with and the folder it runs in; Sentient can't turn that off. In an incognito chat that doesn't use your memory, Sentient tells Claude to ignore those details and takes that email out of replies and actions. |
| Use your **Claude plan's API credits** | The same as any Claude model: requests go to Anthropic with your API key. Sentient never asks for your Claude account login. |
| **Connect OpenRouter** | You sign in on openrouter.ai in your browser and OpenRouter hands Sentient a key for your account, kept in your keychain. Requests then go to OpenRouter, which passes them to the company that runs the model you picked. The model list is fetched from openrouter.ai once connected. |
| **Sign in with ChatGPT** | You sign in on chatgpt.com in your browser and allow Sentient to use your ChatGPT Plus or Pro plan. The first time, OpenAI registers Sentient for your account with a random id for this computer (not your name or email). The sign-in is kept in your keychain and renews on its own. Requests then go to OpenAI (`api.openai.com`) and are not stored there (`store: false`). Usage counts against your plan; see or limit it at chatgpt.com/settings/usage. Sign out removes the sign-in and asks OpenAI to cancel it. |
Expand Down
13 changes: 13 additions & 0 deletions docs/adr/0022-claude-through-your-own-claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ this is counted or allowed."
refused with a plain message, so the role's fallback model takes over. It makes no embeddings. The check-up never
calls it.
- **Stop everything** kills every running Claude Code process tree (a Windows job object or a POSIX process group).
- **Claude Code's own context** (#533). Claude Code adds context of its own next to Sentient's system prompt.
Sentient turns off what it can with documented switches on every run: CLAUDE.md files, auto memory and the git
snapshot (`CLAUDE_CODE_DISABLE_CLAUDE_MDS`, `CLAUDE_CODE_DISABLE_AUTO_MEMORY`,
`CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS`). Two notes stay, because no supported flag, setting or variable removes
them while Claude Code uses the plan login: the signed-in Claude account's email and a note about the computer
(working folder, operating system). In a turn that may not use what Sentient knows about the user
(`ToolContext.memory_allowed` False: an incognito chat with memory off, an app's turn without memory), Claude is
told those notes aren't the user's, and the account email (from `claude auth status`, asked for each call and
kept in memory only) is replaced with "[email hidden]" in the reply, its thinking and tool call arguments, unless
the turn itself contains it. When the account can't be read, every email address the turn didn't contain is
replaced instead. Only whole addresses count, both for what is hidden and for what the turn contains. The
title, running summary, second look and reader of such a turn are private calls too. Ordinary chats are
unchanged.

## Consequences

Expand Down
54 changes: 31 additions & 23 deletions sentient/agent/loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -909,7 +909,7 @@ def interrupted() -> bool:
tool_calls: list[ToolCall] = []
try:
async for chunk in self._model_stream(role, messages, tools, model, source == "chat" and not unprompted,
source):
source, private=not ctx.memory_allowed):
if chunk.thinking:
think_acc += chunk.thinking
yield ThinkingDelta(text=chunk.thinking, **ev)
Expand Down Expand Up @@ -1147,11 +1147,13 @@ def interrupted() -> bool:

async def _model_stream(
self, role: str, messages: list[dict], tools: list[dict] | None, model: str | None, attended: bool,
source: str = "chat",
source: str = "chat", *, private: bool = False,
) -> AsyncIterator[StreamChunk]:
"""``llm.stream``, marked as a reply someone is waiting for only in a chat: Claude Code answers nothing
else (ADR 0022). Its usage is counted under ``source``."""
with claude_code.attended(attended), usage_source(source):
else (ADR 0022). Its usage is counted under ``source``. ``private``: the turn may not use what Sentient
knows about the user (``ToolContext.memory_allowed`` is False), so Claude Code's own account details stay
out of the answer too (#533)."""
with claude_code.attended(attended), claude_code.private(private), usage_source(source):
async for chunk in self.llm.stream(role, messages, tools, model=model):
Comment thread
itsskofficial marked this conversation as resolved.
Resolved
yield chunk

Expand Down Expand Up @@ -2095,8 +2097,10 @@ async def persist(role_: str, content: str | None, **fields: Any) -> None:
), "memory"
)
if self.config.chat.auto_title and session and (session.get("title") or "") == (user_text or "")[:60]:
self._spawn(self._auto_title(session_id, user_text, result.text), "titles")
self._spawn(self._maybe_compress(session_id, flush=not incognito), "memory")
self._spawn(self._auto_title(session_id, user_text, result.text, private=not ctx.memory_allowed),
"titles")
self._spawn(self._maybe_compress(session_id, flush=not incognito, private=not ctx.memory_allowed),
"memory")
if incognito:
log.info("incognito chat %s: nothing learned from this turn", session_id)
elif self.app is not None:
Expand Down Expand Up @@ -2527,15 +2531,17 @@ async def _extract(
except Exception as exc:
log.warning("background extraction failed: %s", exc)

async def _auto_title(self, session_id: str, user_text: str, reply: str) -> None:
async def _auto_title(self, session_id: str, user_text: str, reply: str, *, private: bool = False) -> None:
"""``private``: the turn had no memory, so Claude Code's own account details stay out of the title (#533)."""
try:
title = await self.llm.complete_text(
"fast",
[
{"role": "system", "content": "Write a 2-6 word title for this chat. Title case. No quotes, no punctuation at the end. Reply with the title only."},
{"role": "user", "content": f"User: {user_text[:500]}\nAssistant: {reply[:500]}"},
],
)
with claude_code.private(private):
title = await self.llm.complete_text(
"fast",
[
{"role": "system", "content": "Write a 2-6 word title for this chat. Title case. No quotes, no punctuation at the end. Reply with the title only."},
{"role": "user", "content": f"User: {user_text[:500]}\nAssistant: {reply[:500]}"},
],
)
title = title.strip().strip('"').strip()[:80]
if title:
await self.store.rename_session(session_id, title)
Expand All @@ -2562,10 +2568,11 @@ async def _flush_memory(
except Exception as exc:
log.warning("memory flush before compression failed: %s", exc)

async def _maybe_compress(self, session_id: str, *, flush: bool = True) -> None:
async def _maybe_compress(self, session_id: str, *, flush: bool = True, private: bool = False) -> None:
"""Hermes-style context compression: fold turns that fell out of the history
window into a running summary injected into the system prompt. ``flush=False`` (an incognito chat) keeps
no facts from the folded turns."""
no facts from the folded turns. ``private``: a chat without memory, so Claude Code's own account details stay
out of the summary (#533)."""
try:
total = await self.store.count_messages(session_id)
window = self.config.chat.history_window
Expand All @@ -2589,13 +2596,14 @@ async def _maybe_compress(self, session_id: str, *, flush: bool = True) -> None:
memory_scopes.of_session(self.config, session),
)
prev = (session or {}).get("context_summary") or ""
summary = await self.llm.complete_text(
"fast",
[
{"role": "system", "content": "Update the running summary of a conversation. Keep decisions, facts, names, dates, open questions and anything the assistant promised. Max 250 words. Plain text."},
{"role": "user", "content": f"Current summary:\n{prev or '(none)'}\n\nNew turns:\n{transcript}"},
],
)
with claude_code.private(private):
summary = await self.llm.complete_text(
"fast",
[
{"role": "system", "content": "Update the running summary of a conversation. Keep decisions, facts, names, dates, open questions and anything the assistant promised. Max 250 words. Plain text."},
{"role": "user", "content": f"Current summary:\n{prev or '(none)'}\n\nNew turns:\n{transcript}"},
],
)
await self.store.execute(
"UPDATE sessions SET context_summary = ?, context_upto = ? WHERE id = ?",
(summary.strip(), next_watermark(upto, older, "created_at", "rowid"), session_id),
Expand Down
7 changes: 4 additions & 3 deletions sentient/agent/second_look.py
Original file line number Diff line number Diff line change
Expand Up @@ -812,9 +812,10 @@ async def model(self) -> str:
action = f"{wording['risk_label']}: {rule_label(self.tool, None, self.agent.registry)}"
if wording.get("target"):
action += f" ({wording['target']})"
warning = await model_check(
self.agent.llm, self.request(), action, self.arguments, attended=self.attended, model=self.model_name,
)
with claude_code.private(getattr(self.ctx, "memory_allowed", True) is False): # no account email (#533)
warning = await model_check(
self.agent.llm, self.request(), action, self.arguments, attended=self.attended, model=self.model_name,
)
if warning:
log.info("second look: %s flagged (model): %s", self.tool.name, warning)
else:
Expand Down
3 changes: 2 additions & 1 deletion sentient/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,8 @@ class ChatConfig(BaseModel):
incognito_uses_memory: bool = Field(
True,
description="Incognito chats can use what Sentient already knows about you. Nothing from an incognito chat "
"is ever remembered either way.",
"is ever remembered either way. With Claude through your Claude Code, Claude Code itself still tells Claude "
"your Claude account's email; when this is off, Sentient keeps that email out of replies and actions.",
)
incognito_delete_after_minutes: int = Field(
30,
Expand Down
Loading
Loading