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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ Commands:
exec Execute a command with the secrets of a profile in its environment.
status Show what is loaded: secrets in this shell, SSH keys in the agent, exported profiles.
doctor Check the setup and say how to fix problems.
profile List, show and clear profiles.
profile List, show, clear and prune profiles.
config Create, edit and locate the config file.
init Print the shell integration script for zsh or bash.
```
Expand All @@ -160,7 +160,7 @@ keysafe load github-work -e 4h # one SSH key

Loading a whole profile records it, so its cached secrets are exported in every new shell. Loading individual secrets doesn't.

`unload` undoes `load`: it unsets the variables, deletes the files of file secrets and removes the SSH keys keysafe added from ssh-agent.
`unload` undoes `load`: it unsets the variables, deletes the files of file secrets and removes the SSH keys keysafe added from ssh-agent. That includes secrets loaded before you removed them from the config.

```bash
keysafe unload -p work # the whole profile; new shells no longer get it either
Expand Down Expand Up @@ -200,8 +200,11 @@ keysafe status -p work # one profile
keysafe profile list # profile names
keysafe profile show work # provider, secrets and whether it's exported in new shells (never values)
keysafe profile clear work # delete its cached secrets and forget it was loaded
keysafe profile prune work # delete only what the config no longer names
```

When you remove a secret from the config, its cached value and any SSH key keysafe added stay behind until you prune them. `keysafe doctor` tells you when there is something to prune, including profiles you removed from the config. Pruning doesn't touch open shells: run `keysafe unload` there.

## How It Works

1. **Configuration**: profiles map secret names to `op://` references.
Expand All @@ -218,7 +221,7 @@ keysafe picks up where the zsh-op plugin left off:
- With no config at the new location, `~/.config/op/config.yml` is used, with a hint to move it.
- Profiles recorded in `~/.cache/op` count as loaded until you load them again.
- Secrets cached under `op-secrets-<profile>` move to `keysafe.<profile>` the first time they are read; the old items are deleted.
- Items zsh-op cached that keysafe never reads, such as secrets no longer in your config, stay behind. `keysafe doctor` warns about them and `keysafe profile clear <profile>` deletes them.
- Items zsh-op cached that keysafe never reads, such as secrets no longer in your config, stay behind. `keysafe doctor` warns about them and `keysafe profile prune <profile>` deletes them.

## Troubleshooting

Expand Down
37 changes: 32 additions & 5 deletions src/app/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ const SHOW_EXAMPLES: &str = "Examples:
keysafe profile show work # one profile";
const CLEAR_EXAMPLES: &str = "Examples:
keysafe profile clear work # delete its cached secrets";
const PRUNE_EXAMPLES: &str = "Examples:
keysafe profile prune work # delete what the config no longer names
keysafe profile prune old # everything of a profile removed from the config";
const INIT_CONFIG_EXAMPLES: &str = "Examples:
keysafe config init # create ~/.config/keysafe/config.yml
keysafe config init --force # start over";
Expand Down Expand Up @@ -191,7 +194,7 @@ pub enum ProgramCommand {
name = "unload",
after_help = UNLOAD_EXAMPLES,
about = "Unload secrets of a profile from the current shell.",
long_about = "Undo `load`: unset the environment variables, delete the files of file secrets, and remove the SSH keys keysafe added from ssh-agent. Without names, the whole profile is unloaded and no longer exported in new shells. Its cached secrets stay; use `profile clear` to delete them. Needs the shell integration (`keysafe init`); otherwise, evaluate the printed statements yourself.",
long_about = "Undo `load`: unset the environment variables, delete the files of file secrets, and remove the SSH keys keysafe added from ssh-agent. Without names, the whole profile is unloaded and no longer exported in new shells, including secrets loaded before they were removed from the config. Its cached secrets stay; use `profile clear` to delete them. Needs the shell integration (`keysafe init`); otherwise, evaluate the printed statements yourself.",
next_display_order = 2
)]
Unload(UnloadCommandArgs),
Expand Down Expand Up @@ -246,10 +249,10 @@ pub enum ProgramCommand {
)]
Doctor(DoctorCommandArgs),

/// List, show and clear profiles.
/// List, show, clear and prune profiles.
#[command(
name = "profile",
about = "List, show and clear profiles.",
about = "List, show, clear and prune profiles.",
next_display_order = 8
)]
Profile(ProfileCommandArgs),
Expand Down Expand Up @@ -288,6 +291,7 @@ impl ProgramCommand {
ProfileCommand::List(args) => &mut args.parent,
ProfileCommand::Show(args) => &mut args.parent,
ProfileCommand::Clear(args) => &mut args.parent,
ProfileCommand::Prune(args) => &mut args.parent,
},
Self::Config(args) => match &mut args.command {
ConfigCommand::Init(args) => &mut args.parent,
Expand Down Expand Up @@ -360,10 +364,20 @@ pub enum ProfileCommand {
name = "clear",
after_help = CLEAR_EXAMPLES,
about = "Clear the cached secrets of a profile.",
long_about = "Delete every cached secret of a profile from the keychain and forget that the profile was loaded.",
long_about = "Delete every cached secret of a profile from the keychain and forget that the profile was loaded. The next `load` fetches every secret from 1Password again. To delete only the secrets the config no longer names, use `profile prune`.",
next_display_order = 3
)]
Clear(ProfileClearCommandArgs),

/// Delete what keysafe keeps of secrets removed from the config.
#[command(
name = "prune",
after_help = PRUNE_EXAMPLES,
about = "Delete what keysafe keeps of secrets removed from the config.",
long_about = "Delete the cached secrets of a profile that its config no longer names, and remove the SSH keys keysafe added for them from ssh-agent. The profile stays loaded. For a profile removed from the config, everything keysafe kept of it is deleted. Variables already set in open shells stay; use `unload` there. `keysafe doctor` says when there is something to prune. To delete every cached secret of a profile, use `profile clear`.",
next_display_order = 4
)]
Prune(ProfilePruneCommandArgs),
}

/// Shell specifies a shell supported by the shell integration.
Expand Down Expand Up @@ -697,6 +711,18 @@ pub struct ProfileClearCommandArgs {
pub profile: String,
}

/// ProfilePruneCommandArgs defines the arguments for the ProfilePruneCommand.
#[derive(Debug, Args)]
pub struct ProfilePruneCommandArgs {
/// Shared global flags.
#[command(flatten)]
pub parent: ProgramArgs,

/// Profile whose orphaned secrets are deleted.
#[arg(help = "Profile name (may be one removed from the config).")]
pub profile: String,
}

/// ExportCommandArgs defines the arguments for the ExportCommand.
#[derive(Debug, Args)]
pub struct ExportCommandArgs {
Expand Down Expand Up @@ -968,8 +994,9 @@ mod tests {
};
assert_eq!(args.profile, "work");

// Clearing needs an explicit profile
// Clearing and pruning need an explicit profile
assert!(Program::try_parse_from(["keysafe", "profile", "clear"]).is_err());
assert!(Program::try_parse_from(["keysafe", "profile", "prune"]).is_err());
}

#[test]
Expand Down
Loading
Loading