{"record":{"id":"c36c53ec8323e69a","repo":"warpdotdev/warp","slug":"invalid-repo-format-expected-format-owner","errorCode":null,"errorMessage":"Invalid repo format: '{}'. Expected format: 'owner/repo'","messagePattern":"Invalid repo format: '(.+?)'\\. Expected format: 'owner/repo'","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/src/ai/agent_sdk/environment.rs","lineNumber":48,"sourceCode":"use crate::cloud_object::{CloudObject, CloudObjectLookup as _};\nuse crate::server::cloud_objects::update_manager::{\n    ObjectOperation, OperationSuccessType, UpdateManager, UpdateManagerEvent,\n};\nuse crate::server::ids::{ClientId, ServerId, SyncId};\nuse crate::server::server_api::ServerApiProvider;\nuse crate::util::time_format::format_approx_duration_from_now_utc;\nuse crate::workspaces::user_profiles::UserProfiles;\n\nconst WARP_DEV_ENVIRONMENTS_REPO: &str = \"https://github.com/warpdotdev/warp-dev-environments\";\n\n/// Parse repo strings in the format \"owner/repo\" into GithubRepo objects.\nfn parse_repos(repo_strings: Vec<String>) -> anyhow::Result<Vec<GithubRepo>> {\n    repo_strings\n        .into_iter()\n        .map(|r| {\n            let parts: Vec<&str> = r.split('/').collect();\n            if parts.len() != 2 {\n                return Err(anyhow::anyhow!(\n                    \"Invalid repo format: '{}'. Expected format: 'owner/repo'\",\n                    r\n                ));\n            }\n            Ok(GithubRepo::new(parts[0].to_string(), parts[1].to_string()))\n        })\n        .collect()\n}\n\n/// Handle environment-related CLI commands.\npub fn run(\n    ctx: &mut AppContext,\n    global_options: GlobalOptions,\n    command: EnvironmentCommand,\n) -> anyhow::Result<()> {\n    let runner = ctx.add_singleton_model(|_ctx| EnvironmentCommandRunner);\n    match command {\n        EnvironmentCommand::List => {","sourceCodeStart":30,"sourceCodeEnd":66,"githubUrl":"https://github.com/warpdotdev/warp/blob/e72fd7aacbbb2236d9b3be2aad7e7178fe94b4bc/app/src/ai/agent_sdk/environment.rs#L30-L66","documentation":"Thrown by parse_repos() in app/src/ai/agent_sdk/environment.rs when a --repo argument cannot be split on '/' into exactly two non-empty parts ('owner' and 'repo'). The value is converted into a GithubRepo via GithubRepo::new(parts[0], parts[1]), so any string that is not a bare 'owner/repo' slug is rejected before any network or Warp Drive work starts. It surfaces as a fatal CLI error from the environment create/update commands, which call parse_repos on the repo and remove_repo arguments.","triggerScenarios":"Running `warp environment create --repo <value>` or `warp environment update --repo <value>` where value is a full GitHub URL (e.g. https://github.com/warpdotdev/warp-dev-environments splits into 5+ parts), a bare repo name ('myrepo' splits into 1 part), a trailing/leading slash ('owner/repo/' splits into 3 parts), or an empty string.","commonSituations":"Users pasting a repo URL copied from the browser instead of the owner/repo slug; scripts that pass git remotes (git@github.com:owner/repo.git) verbatim; shell quoting that swallows or adds slashes; trailing slash after tab-completion.","solutions":["Pass the bare slug: `--repo owner/repo` (e.g. `--repo warpdotdev/warp-dev-environments`), not a URL or SSH remote","If your source is a URL, strip the scheme and host first (keep only the last two path segments)","Check for trailing/leading slashes and empty strings in scripted arguments before invoking the CLI","If you must accept URLs in tooling, pre-validate with a split('/').len() == 2 check and normalize to owner/repo"],"exampleFix":"# before\nwarp environment create --name dev --repo https://github.com/warpdotdev/warp-dev-environments\n\n# after\nwarp environment create --name dev --repo warpdotdev/warp-dev-environments","handlingStrategy":"validation","validationCode":"// Rust: validate before calling the CLI/parse_repos\nfn is_valid_repo_spec(s: &str) -> bool {\n    let parts: Vec<&str> = s.split('/').collect();\n    parts.len() == 2 && !parts[0].is_empty() && !parts[1].is_empty()\n}\n\n// Normalize a pasted URL to owner/repo before passing it\nfn normalize_repo_input(s: &str) -> Option<String> {\n    let slug = s.trim().trim_start_matches(\"https://github.com/\")\n        .trim_start_matches(\"http://github.com/\");\n    let parts: Vec<&str> = slug.split('/').collect();\n    if parts.len() >= 2 {\n        Some(format!(\"{}/{}\", parts[parts.len() - 2], parts[parts.len() - 1]))\n    } else {\n        None\n    }\n}","typeGuard":"fn is_owner_repo_slug(s: &str) -> bool {\n    matches!(s.split('/').collect::<Vec<_>>()[..], [o, r] if !o.is_empty() && !r.is_empty())\n}","tryCatchPattern":"// parse_repos returns anyhow::Result — handle it at the call site\nmatch parse_repos(repo_strings) {\n    Ok(repos) => { /* proceed */ },\n    Err(err) => eprintln!(\"bad --repo argument: {err}\"),\n}","preventionTips":["Always pass the bare 'owner/repo' slug, never a full URL or SSH remote","In scripts, run a split('/').len() == 2 assertion on every --repo value before invoking the CLI","Strip whitespace from interpolated variables holding repo values","Document the expected format wherever the CLI is scripted for others"],"tags":["rust","cli","validation","github","argument-parsing"],"backgroundTag":null,"analyzedSha":"e72fd7aacbbb2236d9b3be2aad7e7178fe94b4bc","analyzedAt":"2026-08-16T08:27:25.381Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}