Skip to content

Add workspace trust support#15177

Merged
archseer merged 4 commits into
helix-editor:masterfrom
xe-nul:workspace_trust
Mar 26, 2026
Merged

Add workspace trust support#15177
archseer merged 4 commits into
helix-editor:masterfrom
xe-nul:workspace_trust

Conversation

@xe-nul

@xe-nul xe-nul commented Jan 23, 2026

Copy link
Copy Markdown
Contributor

Should fix #2697

This isn't quite ready to be pushed to main branch, but it does work (on my machine).

The biggest thing that it lacks is some kind of message that CWD is not in the list of trusted workspaces with an instruction on how to change that (I think that single prompt like "Current directory is not trusted; run ':workspace_trust' to enable all features' might be sufficient), but I couldn't figure out how to do that just yet.

Smaller flaws:

  • ':workspace_trust' doesn't retroactively start LSP client in buffers and doesn't automatically reload the config (I don't know if we even want that, and LSP part might not be so simple AFAICT)
  • I think there should be a warning if user tries to add whole '/', or '/home/', or something equally bad to their trusted list
  • It doesn't disable tree-sitter (again, I am not sure if we want that, tree-sitter might be an attack vector, but I am not sure how proven that is. Perhaps, it should be a config option?)

@xe-nul

xe-nul commented Jan 23, 2026

Copy link
Copy Markdown
Contributor Author

Now it does show a warning when a new file is open. I don't really like that the message disappears on a first keypress since that makes it too easy to miss if one is not familiar with the feature, but I think it is not hostile to unsuspecting user any more.

@xe-nul xe-nul marked this pull request as ready for review January 23, 2026 21:27
@the-mikedavis

Copy link
Copy Markdown
Member

Also see #14668. I haven't found the time lately but the Select component can be cleanly applied to master and provide LSP window/showMessageRequest in addition to this use-case

@xe-nul

xe-nul commented Jan 23, 2026

Copy link
Copy Markdown
Contributor Author

Yeah, Select feels much better, I'll work on it

@xe-nul xe-nul marked this pull request as draft January 23, 2026 23:03
@xe-nul

xe-nul commented Jan 23, 2026

Copy link
Copy Markdown
Contributor Author

I thought about it a bit, and I think Select approach is really good for discoverability of this feature: you can't miss it (we can safely add it to master/next release and people will not be surprised that their LSP doesn't work), and my current approach just doesn't get in the way (Select isn't that much obtrusive, but still it can be a bit unwieldy to click one more key when you open something like a config file to fix something quickly).

The way I see it, the default should be Select pop-up, and my current approach should be behind a config option, I'll try to do just that

@xe-nul

xe-nul commented Jan 24, 2026

Copy link
Copy Markdown
Contributor Author

Sorry about big messy PR, but I think it does what it needs to now.

Now, by default, it will prompt the user with Select UI element from @the-mikedavis's branch, I added this element with a separate commit, hope that will make it easier to review.

There is a config option editor.trust-selector that accepts two options: select (the default one) and simple. Simple is basically my initial implementation, for the case where user doesn't want to be explicitly promted about workspace trust every time they open a file in a directory for the first time.

Notes:

  • my implementation keeps it's files (trusted_workspaces and untrusted_workspaces) in XDG data directory, not in state directory, because state directory is only present with XDG strategy in etcetera lib, and on Windows strategy.state_dir() will (I didn't really test it, but I think it will) return None, so I think it's best to leave these files in data_dir for now ('~/.local/share/helix' on *nix)
  • LSP doesn't automatically start and config isn't automatically reloaded after trusting the workspace with either :workspace_trust or Select menu
  • it's kinda hacky and rough in some places

But I think I am mostly happy with it now, so I'll wait for some input.

Also see #14668. I haven't found the time lately but the Select component can be cleanly applied to master and provide LSP window/showMessageRequest in addition to this use-case

With Select in place, I think I can try to port this LSP message impl from that branch, but I think I better do it in a separate PR?

@xe-nul xe-nul marked this pull request as ready for review January 24, 2026 13:27
@xe-nul

This comment was marked as resolved.

@xe-nul xe-nul force-pushed the workspace_trust branch 2 times, most recently from b937531 to 5d85500 Compare January 24, 2026 16:38
@xe-nul

xe-nul commented Jan 24, 2026

Copy link
Copy Markdown
Contributor Author

I've added an editor.trust-selector = "always-trust-lsp" option. It does what it says it does, but it would be great to add an option to restore old behavior: always trust everything. That would be kinda annoying to do with config.toml and languages.toml, so I decided not to do that right now

@xe-nul xe-nul changed the title WIP: Add rudimentary workspace trust support Addworkspace trust support Jan 24, 2026
@xe-nul xe-nul changed the title Addworkspace trust support Add workspace trust support Jan 24, 2026
@xe-nul xe-nul force-pushed the workspace_trust branch 3 times, most recently from 0869ce5 to 682a197 Compare January 24, 2026 19:15
@xe-nul

xe-nul commented Jan 24, 2026

Copy link
Copy Markdown
Contributor Author

Alright, now both :workspace_trust and 'Always trust' option in Select menu do start LSP servers.

I have also added docs.

I think this thing is really ready to review now

Comment thread helix-term/src/commands/typed.rs Outdated
},
},
TypableCommand {
name: "workspace_trust",

@m4rch3n1ng m4rch3n1ng Jan 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-        name: "workspace_trust",
+        name: "workspace-trust",

typable commands in helix are kebab-case by convention and not snake_case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I think it is fixed everywhere now

@the-mikedavis

Copy link
Copy Markdown
Member

I finally applied the Select component to master so this PR can be rebased on those changes now, 76c23b3. It's nearly the same as it was in my branch but I added some horizontal padding to the message

@xe-nul

xe-nul commented Jan 25, 2026

Copy link
Copy Markdown
Contributor Author

Just FYI, but I see a warning when building current master with nix build github:helix-editor/helix:

helix-term> warning: non-local `impl` definition, `impl` blocks should be written at the same level as their item
helix-term>     --> helix-term/src/application.rs:1121:25
helix-term>      |
helix-term> 759  |       ) {
helix-term>      |  _______-
helix-term> 760  | |         use helix_lsp::{Call, MethodCall, Notification};
helix-term> 761  | |
helix-term> 762  | |         macro_rules! language_server {
helix-term> ...    |
helix-term> 1121 | |                         impl ui::menu::Item for lsp::MessageActionItem {
helix-term>      | |                         ^^^^^--------------^^^^^----------------------
helix-term>      | |                              |                  |
helix-term>      | |                              |                  `MessageActionItem` is not local
helix-term>      | |                              `Item` is not local
helix-term> ...    |
helix-term> 1174 | |     }
helix-term>      | |_____- move the `impl` block outside of this async fn `<unnameable>` and up 2 bodies
helix-term>      |
helix-term>      = note: an `impl` is never scoped, even when it is nested inside an item, as it may impact type checking outside of that item, which can be the case if neither the trait or the self type are at the same nesting level as the `impl`
helix-term>      = note: `#[warn(non_local_definitions)]` on by default
helix-term> warning: hiding a lifetime that's elided elsewhere is confusing
helix-term>     --> helix-term/src/application.rs:1123:39
helix-term>      |
helix-term> 1123 | ...                   fn format(&self, _data: &Self::Data) -> tui::widgets::Row {
helix-term>      |                                 ^^^^^                         ----------------- the same lifetime is hidden here
helix-term>      |                                 |
helix-term>      |                                 the lifetime is elided here
helix-term>      |
helix-term>      = help: the same lifetime is referred to in inconsistent ways, making the signature confusing
helix-term>      = note: `#[warn(mismatched_lifetime_syntaxes)]` on by default
helix-term> help: use `'_` for type paths
helix-term>      |
helix-term> 1123 |                             fn format(&self, _data: &Self::Data) -> tui::widgets::Row<'_> {
helix-term>      |                                                                                      ++++
helix-term> warning: `helix-term` (lib) generated 2 warnings
helix-term>     Finished `release` profile [optimized] target(s) in 2m 37s

I only see this warning when building with nix, cargo clippy for example doesn't produce this warning. I already saw something similar earlier, is it possible that nix build and nix develop are using different toolchain versions?

@xe-nul

xe-nul commented Jan 25, 2026

Copy link
Copy Markdown
Contributor Author

All right, I rebased it on top of master and fixed a bug in the implementation that I've just found. Should be ready for review again

@the-mikedavis

Copy link
Copy Markdown
Member

Ah yeah, when building the package we use the latest stable if I remember correctly, so it shows warnings which aren't emitted by the MSRV. And the devShell is using the MSRV. I'll push some fixes for more recent rustc / clippy

Comment on lines +24 to +34
match fs::read_to_string(workspace_trust_file()) {
Ok(workspace_trust_file) => {
for line in workspace_trust_file.split('\n') {
if !line.is_empty() {
let path = PathBuf::from(line);
trusted.insert(path);
}
}
}
Err(e) => log::error!("workspace file couldn't be read: {:?}", e),
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern seems repeated multiple times

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be more efficient to read the file in a streaming way, returning a line iterator we can scan through

@xe-nul xe-nul Jan 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now load() is used only when there is a need to write changes to trust-related files. For checking the trust there are quick_query_workspace() and quick_query_workspace_with_explicit_untrust() functions now, they do basically what you've described.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed repeated pattern now too

@Rudxain Rudxain Apr 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quick_query_workspace() and quick_query_workspace_with_explicit_untrust() functions now, they do basically what you've described.

Well, sort of. They both use read_to_string which blocks until the entire file has been loaded into memory. If the files were line-buffered (e.g. BufRead::lines) then the iterator can yield the lines immediately as they arrive from storage.

This is why I'm concerned about perf

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#15590 should address the comments above

Comment thread helix-loader/src/workspace_trust.rs Outdated
}
}

fn write_untrust_to_file(&self) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should be just one trust file, and untrust only removes entries. Users should either opt to trust all workspaces, or selectively trust. Blacklisting specific entries is too error prone because if you forget you're vulnerable. Same with opening the workspace then calling :untrust on it: by that point it's already too late since the config got executed

@xe-nul xe-nul Jan 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with only one trust file initially, and I added second blacklist file after I copied Select UI approach from Mike's branch since that has Not now and Never options, and for Never to work we need to parse an additional file.

When trust-selector = "simple" config option is set, it doesn't check that second file since it doesn't really need to ("simple" is basically implementation of your ideas from #2697 (comment)).

:workspace-untrust is more of a convenience feature that you would only use because it is easier than modifying trust file by hand. Every workspace is still not trusted until user says it is.

If we want to remove second file, we need to remove Never option from the default trust selector approach, and I am not sure yet that we want that

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there should just be two modes: insecure=true that trusts everything and the default where each workspace has to be explicitly appproved. This should match VSCode behaviour

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right, I just changed it.

I wanted to have a middle ground between trusting everything and having to click through a pop-up, but Select is really good, I lived for a week with it and it doesn't get in the way.

@xe-nul xe-nul force-pushed the workspace_trust branch 2 times, most recently from b0ccafc to ffb8195 Compare January 26, 2026 14:25
@xe-nul

xe-nul commented Jan 29, 2026

Copy link
Copy Markdown
Contributor Author

I renamed untrusted_workspaces file to excluded_workspaces, hope this will make it more clear what that file does.

@xe-nul xe-nul force-pushed the workspace_trust branch 3 times, most recently from c004ccb to b84fe28 Compare February 1, 2026 16:05
@xe-nul xe-nul force-pushed the workspace_trust branch 2 times, most recently from 1bd9d84 to 07e4a44 Compare March 11, 2026 16:47
@archseer archseer requested a review from the-mikedavis March 17, 2026 05:32
@xe-nul xe-nul force-pushed the workspace_trust branch 2 times, most recently from 52af224 to 20849a8 Compare March 25, 2026 10:34
Comment thread book/src/workspace-trust.md Outdated
Comment thread book/src/workspace-trust.md Outdated
Tested - the path is the same on Linux and macOS
the-mikedavis
the-mikedavis previously approved these changes Mar 26, 2026
archseer
archseer previously approved these changes Mar 26, 2026
@the-mikedavis the-mikedavis dismissed stale reviews from archseer and themself via 4b0cd82 March 26, 2026 16:32
@archseer archseer merged commit 0475fdd into helix-editor:master Mar 26, 2026
7 checks passed
@@ -0,0 +1,23 @@
# Workspace trust

Helix has a number of potentially dangerous features, namely LSP and ability to use local to workspace configurations. Those features can lead to unexpected code execution. To protect against code execution in dangerous contexts, Helix has a workspace trust protection, which will prevent these potentially dangerous features from running automatically.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for this PR!

I am not sure to understand the doc:

Will untrusted workspaces not have any LSP enabled (even if globally set) and not local config (which can contain LSP and language settings)?

Or are the global LSP always enabled?

I would suggest at the end:

Helix has a workspace trust protection, which will disable the following features:

  • loading config the the workspace’s .helix folder (language matching, LSP…)
  • running the globally configured LSPs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Btw “ability to use local to workspace configurations” reads convoluted)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, nothing can load local configs or start an LSP server/client until workspace is trusted. I will try to do something about the docs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that look better to you?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I find it clearer. Thanks!

Regarding “trust”, the child folders of trusted workspaces are also trusted, right?

So if I trust ~/work, every (sub-)subfolder in it will be automatically trusted, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not in the way you expect it to work.

This is the function that selects the workspace:

pub fn find_workspace() -> (PathBuf, bool) {

If your ~/work is a git repository, then yeah. If not, then you'll have to trust each repository in it when needed.

Originally, I did exactly what you described: walking directory tree up and looking if any parent is trusted, but that is not only slower, but a giant footgun too: you can open a file in your /home, allow trust there, forget about it, and then basically everything you open will be trusted. IMO better design is to make prompt about trust as unobtrusive as possible, which I hope this PR achieves

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for the detailed explanation!

I appreciate the vscode prompt, which offers a nice middle ground I think: the ability to trust one parent level (ie all direct children of ~/work could be marked as trusted, but sub-children must be trusted explicitly).

Note that I am already satisfied with this feature, and just want to give some (hopefully constructive) feedback ❤️

OlegOAndreev pushed a commit to OlegOAndreev/helix that referenced this pull request Mar 27, 2026
* Workspace trust: init

* Apply suggestion from @archseer

* Apply suggestion from @the-mikedavis

Tested - the path is the same on Linux and macOS

* Avoid logging when workspace trust files can't be read from NotFound

---------

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
@HigherOrderLogic

Copy link
Copy Markdown

I am making a PR to add "only once" trust option, does it makes sense? Just want to gauge the opinion before actually opening a PR.

@the-mikedavis

the-mikedavis commented Apr 4, 2026

Copy link
Copy Markdown
Member

I think it would make sense to have an option that trusts the current config. We'd take a cryptographic hash of the config and store it in the trust file(s). That would prevent future git pulls from surprising you with config changes and let you trust whatever is there that you have already vetted.

In the wild I know https://github.com/direnv/direnv works like this.

ymgyt pushed a commit to ymgyt/helix that referenced this pull request May 2, 2026
* Workspace trust: init

* Apply suggestion from @archseer

* Apply suggestion from @the-mikedavis

Tested - the path is the same on Linux and macOS

* Avoid logging when workspace trust files can't be read from NotFound

---------

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] workspace trust: language servers & tree sitter

10 participants