mirror of
https://github.com/storytold/craft-anything.git
synced 2026-11-05 20:47:45 +01:00
Give Claude Code a writable conversation folder under Flatpak, and say how to sign in there (#561)
Fixes #506. Under a Flatpak, where ~/.claude is read-only, Claude Code is started with a writable CLAUDE_CONFIG_DIR in the app's data folder so its conversations are saved and --resume works; the sign-in message says how to sign in inside the sandbox. A CLAUDE_CONFIG_DIR the person sets wins, and SSH builds are unchanged.
This commit is contained in:
@@ -380,10 +380,31 @@ impl AgentAdapter for ClaudeCode {
|
||||
}
|
||||
|
||||
/// Claude Code's own folder: `CLAUDE_CONFIG_DIR` when it's set (the person's choice, which the
|
||||
/// agent we start keeps too), else `~/.claude`.
|
||||
/// agent we start keeps too), else our own under a Flatpak (see [`flatpak_claude_dir`]), else
|
||||
/// `~/.claude`.
|
||||
fn claude_config_dir() -> Option<std::path::PathBuf> {
|
||||
let set = std::env::var_os("CLAUDE_CONFIG_DIR").filter(|v| !v.is_empty()).map(std::path::PathBuf::from);
|
||||
set.or_else(|| dirs::home_dir().map(|h| h.join(".claude")))
|
||||
set.or_else(flatpak_claude_dir).or_else(|| dirs::home_dir().map(|h| h.join(".claude")))
|
||||
}
|
||||
|
||||
/// Whether this app runs in a Flatpak sandbox (it sets `FLATPAK_ID` and makes `/.flatpak-info`).
|
||||
pub fn in_flatpak() -> bool {
|
||||
static IN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*IN.get_or_init(|| std::env::var_os("FLATPAK_ID").is_some() || std::path::Path::new("/.flatpak-info").exists())
|
||||
}
|
||||
|
||||
/// Where Claude Code keeps its conversations when we start it under a Flatpak and the person
|
||||
/// hasn't chosen a folder: its own is `~/.claude`, which the sandbox only lets us read, so a
|
||||
/// conversation was never saved and `--resume` found nothing (#506). A folder of ours in the
|
||||
/// app's data folder is writable, and kept with the app. `None` outside a Flatpak, or when
|
||||
/// `CLAUDE_CONFIG_DIR` is set.
|
||||
pub fn flatpak_claude_dir() -> Option<std::path::PathBuf> {
|
||||
let chosen = std::env::var_os("CLAUDE_CONFIG_DIR").is_some_and(|v| !v.is_empty());
|
||||
flatpak_claude_dir_in(in_flatpak(), chosen, dirs::data_dir())
|
||||
}
|
||||
|
||||
pub fn flatpak_claude_dir_in(flatpak: bool, chosen: bool, data: Option<std::path::PathBuf>) -> Option<std::path::PathBuf> {
|
||||
(flatpak && !chosen).then_some(data?.join("craft-anything").join("claude"))
|
||||
}
|
||||
|
||||
/// Claude Code's name for the folder a session ran in: the path with everything but letters and
|
||||
|
||||
@@ -185,13 +185,28 @@ pub fn verify_flags(program: &str, help_args: &[&str], needed: &[&str]) -> bool
|
||||
/// The fix for a sign-in failure, in plain words. Sign-in always happens in Claude Code itself;
|
||||
/// we never see or handle the account.
|
||||
pub fn sign_in_hint(message: &str) -> Option<String> {
|
||||
let m = message.to_ascii_lowercase();
|
||||
["authenticat", "oauth", "log in", "login", "not logged", "api key", "unauthorized"]
|
||||
.iter()
|
||||
.any(|w| m.contains(w))
|
||||
.then(|| "Sign in to Claude Code: open a terminal and run `claude auth login` (or start `claude` and type /login), then try again.".to_string())
|
||||
sign_in_hint_in(message, adapters::in_flatpak())
|
||||
}
|
||||
|
||||
/// [`sign_in_hint`] for a Flatpak or not. In a Flatpak, Claude Code keeps its sign-in and
|
||||
/// conversations in a folder of the app's own (#506), so signing in from a normal terminal signs
|
||||
/// in the other copy: it's done through the sandbox, with that folder.
|
||||
fn sign_in_hint_in(message: &str, flatpak: bool) -> Option<String> {
|
||||
let m = message.to_ascii_lowercase();
|
||||
let wants_sign_in = ["authenticat", "oauth", "log in", "login", "not logged", "api key", "unauthorized"].iter().any(|w| m.contains(w));
|
||||
if !wants_sign_in {
|
||||
return None;
|
||||
}
|
||||
Some(if flatpak {
|
||||
format!("Sign in to Claude Code inside this app's sandbox: open a terminal and run `{FLATPAK_SIGN_IN}`, then try again.")
|
||||
} else {
|
||||
"Sign in to Claude Code: open a terminal and run `claude auth login` (or start `claude` and type /login), then try again.".to_string()
|
||||
})
|
||||
}
|
||||
|
||||
/// Signing in for the Claude Code that a Flatpak build starts: the same folder it will use.
|
||||
const FLATPAK_SIGN_IN: &str = "flatpak run --command=sh ai.storyteller.craft-anything -c 'CLAUDE_CONFIG_DIR=\"$XDG_DATA_HOME/craft-anything/claude\" \"$HOME/.local/bin/claude\" auth login'";
|
||||
|
||||
/// Words in an API's answer when the installed Claude Code is older than the model it was asked
|
||||
/// to use (#165).
|
||||
pub const TOO_OLD_FOR_MODEL: &str = "does not support this model";
|
||||
@@ -561,7 +576,15 @@ fn run_cli(spec: &CliSpec, text: &str, tx: &Sender<AgentEvent>, stop: &Stop, sta
|
||||
// raised to match, so nothing cuts a long clone short (its defaults stop a quiet tool call at
|
||||
// 30 minutes and a background build at 10).
|
||||
let (turn, quiet) = if builder { (24 * 3600, 8 * 3600) } else { (30 * 60, 15 * 60) };
|
||||
let env = agent_env(builder, spec.options.effort);
|
||||
let mut env = agent_env(builder, spec.options.effort);
|
||||
// Under a Flatpak, Claude Code gets a folder it can write its conversations to (#506); on
|
||||
// another computer over SSH it keeps its own.
|
||||
if remote.is_none()
|
||||
&& let Some(dir) = adapters::flatpak_claude_dir()
|
||||
{
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
env.push(("CLAUDE_CONFIG_DIR", dir.to_string_lossy().into_owned()));
|
||||
}
|
||||
let (prog, argv) = match &remote {
|
||||
Some(host) => ("ssh".to_string(), crate::ssh::ssh_args(host, &remote_command(&program, args, &env))),
|
||||
None => (program.clone(), args),
|
||||
@@ -823,6 +846,23 @@ impl Drop for CliConversation {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// #506: under a Flatpak `~/.claude` is read-only, so Claude Code gets a folder of ours to save
|
||||
/// its conversations in (so `--resume` finds them); the person's own `CLAUDE_CONFIG_DIR` and a
|
||||
/// normal install are left as they are, and the sign-in hint says how to sign in there.
|
||||
#[test]
|
||||
fn under_a_flatpak_claude_code_keeps_its_conversations_in_a_folder_it_can_write() {
|
||||
let data = || Some(std::path::PathBuf::from("/home/u/.var/app/ai.storyteller.craft-anything/data"));
|
||||
let dir = adapters::flatpak_claude_dir_in(true, false, data()).unwrap();
|
||||
assert!(dir.ends_with("craft-anything/claude") && dir.starts_with("/home/u/.var/app/ai.storyteller.craft-anything/data"), "{dir:?}");
|
||||
assert_eq!(adapters::flatpak_claude_dir_in(true, true, data()), None, "their own choice stays");
|
||||
assert_eq!(adapters::flatpak_claude_dir_in(false, false, data()), None, "a normal install keeps ~/.claude");
|
||||
assert_eq!(adapters::flatpak_claude_dir_in(true, false, None), None);
|
||||
let hint = sign_in_hint_in("Not logged in", true).unwrap();
|
||||
assert!(hint.contains("flatpak run") && hint.contains("CLAUDE_CONFIG_DIR") && hint.contains("auth login"), "{hint}");
|
||||
assert!(sign_in_hint_in("Not logged in", false).unwrap().contains("`claude auth login`"));
|
||||
assert_eq!(sign_in_hint_in("all fine", true), None);
|
||||
}
|
||||
|
||||
/// Over SSH our environment doesn't reach the other computer, so the chosen effort (and Claude
|
||||
/// Code's raised limits) go in the remote command itself.
|
||||
#[test]
|
||||
|
||||
@@ -30,6 +30,23 @@ integration does, why it is shaped this way, and what remains to confirm.
|
||||
- Before a build starts, `claude auth status` is checked. If Claude Code isn't signed in, the run
|
||||
stops with: "Sign in to Claude Code: open a terminal and run `claude auth login`".
|
||||
|
||||
## Under Flatpak
|
||||
|
||||
The Flatpak build can read the home folder but not write to it, so Claude Code can't save its
|
||||
conversations in `~/.claude`: the first turn ran, and `--resume` then said "No conversation found
|
||||
with session ID" (#506). When the app runs in a Flatpak and `CLAUDE_CONFIG_DIR` isn't set, it starts
|
||||
Claude Code with `CLAUDE_CONFIG_DIR` pointing at `craft-anything/claude` in the app's own data
|
||||
folder (`~/.var/app/ai.storyteller.craft-anything/data/`), which is writable. Two consequences:
|
||||
|
||||
- Claude Code there is signed in separately from the one on the host. The sign-in message gives the
|
||||
command that does it through the sandbox:
|
||||
`flatpak run --command=sh ai.storyteller.craft-anything -c 'CLAUDE_CONFIG_DIR="$XDG_DATA_HOME/craft-anything/claude" "$HOME/.local/bin/claude" auth login'`
|
||||
(Craft Anything never reads Claude Code's credentials, so they aren't copied over.)
|
||||
- Setting `CLAUDE_CONFIG_DIR` yourself, for example with `flatpak override --user --env=…`, wins.
|
||||
Builds on another computer over SSH are not changed.
|
||||
|
||||
Not yet checked in a real Flatpak (the tests cover the choice of folder and the message).
|
||||
|
||||
## Why this is within Anthropic's terms (as published, October 2026)
|
||||
|
||||
From Claude Code's *Legal and compliance* page:
|
||||
|
||||
Reference in New Issue
Block a user