## File: README.md

Vouch

--- People must be **vouched for** before interacting with certain parts of a project (the exact parts are configurable to the project to enforce). People can also be explicitly **denounced** to block them from interacting with the project. The implementation is generic and can be used by any project on any code forge, but we provide **GitHub integration** out of the box via GitHub actions and the CLI. The vouch list is maintained in a single flat file using a minimal format that can be trivially parsed using standard POSIX tools and any programming language without external libraries. This project is based on the already successful system in use by the [Pi project](https://github.com/badlogic/pi-mono). I took what they already do and extracted it into a more generalizable system with some other changes. > [!WARNING] > > This is an experimental system in use by [Ghostty](https://github.com/ghostty-org/ghostty). > We'll continue to improve the system based on experience and feedback. ## Why? Open source has always worked on a system of _trust and verify_. Historically, the effort required to understand a codebase, implement a change, and submit that change for review was high enough that it naturally filtered out many low quality contributions from unqualified people. For over 20 years of my life, this was enough for my projects as well as enough for most others. Unfortunately, the landscape has changed particularly with the advent of AI tools that allow people to trivially create plausible-looking but extremely low-quality contributions with little to no true understanding. Contributors can no longer be trusted based on the minimal barrier to entry to simply submit a change. But, open source still works on trust! And every project has a definite group of trusted individuals (maintainers) and a larger group of probably trusted individuals (active members of the community in any form). So, let's move to an explicit trust model where trusted individuals can vouch for others, and those vouched individuals can then contribute. ## Who is Vouched? **Who** and **how** someone is vouched or denounced is left entirely up to the project integrating the system. Additionally, **what** consequences a vouched or denounced person has is also fully up to the project. Implement a policy that works for your project and community. ## Usage ### GitHub Integrating vouch into a GitHub project is easy with the [provided GitHub Actions](https://github.com/mitchellh/vouch/tree/main/action). By choosing which actions to use, you can fully control how users are vouched and what they can or can't do. For an example, look at this repository! It fully integrates vouch. Below is a list of the actions and a brief description of their function. See the linked README in the action directory for full usage details. | Action | Trigger | Description | | ------------------------------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [check-issue](action/check-issue/README.md) | `issues` | Check if an issue author is vouched on open or reopen. Bots and collaborators with write access are automatically allowed. Optionally auto-close and lock issues from unvouched or denounced users. | | [check-pr](action/check-pr/README.md) | `pull_request_target` | Check if a PR author is vouched on open or reopen. Bots and collaborators with write access are automatically allowed. Optionally auto-close PRs from unvouched or denounced users. | | [check-user](action/check-user/README.md) | Any | Check if a GitHub user is vouched. Outputs the user's status and fails the step by default if the user is not vouched. Set `allow-fail` to only report via output. | | [manage-by-discussion](action/manage-by-discussion/README.md) | `discussion_comment` | Let collaborators vouch, denounce, or unvouch users via discussion comments. Updates the vouched file and commits the change. | | [manage-by-issue](action/manage-by-issue/README.md) | `issue_comment` | Let collaborators vouch or denounce users via issue comments. Updates the vouched file and commits the change. | | [sync-codeowners](action/sync-codeowners/README.md) | Any | Sync CODEOWNERS owners into the vouch list by vouching missing users. | | [setup-vouch](action/setup-vouch/README.md) | Any | Install the `vouch` CLI on `PATH`. Nushell is installed automatically if not already available. | ### CLI The CLI is implemented as a Nushell module and only requires Nushell to run. There are no other external dependencies. #### Integrated Help This is Nushell, so you can get help on any command: ```nu use vouch * help add help check help denounce help gh-check-issue help gh-check-pr help gh-manage-by-issue ``` #### Local Commands **Check a user's vouch status:** ```bash vouch check ``` Exit codes: 0 = vouched, 1 = denounced, 2 = unknown. **Add a user to the vouched list:** ```bash # Preview new file contents (default) vouch add someuser # Write the file in-place vouch add someuser --write ``` **Denounce a user:** ```bash # Preview new file contents (default) vouch denounce badactor # With a reason vouch denounce badactor --reason "Submitted AI slop" # Write the file in-place vouch denounce badactor --write ``` #### GitHub Integration Requires the `GITHUB_TOKEN` environment variable. If not set and `gh` is available, the token from `gh auth token` is used. **Check if an issue author is vouched:** ```bash # Check issue author status (dry run) vouch gh-check-issue 123 --repo owner/repo # Auto-close unvouched issues (dry run) vouch gh-check-issue 123 --repo owner/repo --auto-close # Actually close unvouched issues vouch gh-check-issue 123 --repo owner/repo --auto-close --dry-run=false # Allow unvouched users, only block denounced vouch gh-check-issue 123 --repo owner/repo --require-vouch=false --auto-close ``` Outputs status: `skipped` (bot/collaborator), `vouched`, `allowed`, or `closed`. **Check if a PR author is vouched:** ```bash # Check PR author status (dry run) vouch gh-check-pr 123 --repo owner/repo # Auto-close unvouched PRs (dry run) vouch gh-check-pr 123 --repo owner/repo --auto-close # Actually close unvouched PRs vouch gh-check-pr 123 --repo owner/repo --auto-close --dry-run=false # Allow unvouched users, only block denounced vouch gh-check-pr 123 --repo owner/repo --require-vouch=false --auto-close ``` Outputs status: `skipped` (bot/collaborator), `vouched`, `allowed`, or `closed`. **Manage contributor status via issue comments:** ```bash # Dry run (default) vouch gh-manage-by-issue 123 456789 --repo owner/repo # Actually perform the action vouch gh-manage-by-issue 123 456789 --repo owner/repo --dry-run=false ``` Responds to comments from collaborators with sufficient role (admin, maintain, write, or triage by default): - `vouch` — vouches for the issue author - `vouch @user` — vouches for a specific user - `vouch ` — vouches for the issue author with a reason - `vouch @user ` — vouches for a specific user with a reason - `denounce` — denounces the issue author - `denounce @user` — denounces a specific user - `denounce ` — denounces the issue author with a reason - `denounce @user ` — denounces a specific user with a reason Keywords are customizable via `--vouch-keyword` and `--denounce-keyword`. You can also allow specific managers listed in a separate VOUCHED file via `--vouched-managers`. Outputs status: `vouched`, `denounced`, or `unchanged`. ### Library The module also exports a `lib` submodule for scripting: ```nu use vouch/lib.nu * let records = open VOUCHED.td $records | check-user "mitchellh" --default-platform github # "vouched", "denounced", or "unknown" $records | add-user "newuser" # returns updated table $records | denounce-user "badactor" "reason" # returns updated table $records | remove-user "olduser" # returns updated table ``` ## Vouched File Format The vouch list is stored in a `.td` file. See [VOUCHED.example.td](VOUCHED.example.td) for an example. The file is looked up at `VOUCHED.td` or `.github/VOUCHED.td` by default. ``` # Comments start with # username platform:username -platform:denounced-user -platform:denounced-user reason for denouncement ``` - One handle per line (without `@`), sorted alphabetically. - Optionally specify a platform prefix: `platform:username` (e.g., `github:mitchellh`). - Denounce a user by prefixing with `-`. - Optionally add details after a space following the handle. The `from td` and `to td` commands are exported by the module, so Nushell's `open` command works natively with `.td` files to decode into structured tables and encode back to the file format with comments and whitespace preserved. > [!NOTE] > > **What is `.td`?** This stands for "Trustdown," a play on the > word "Markdown." I intend to formalize a specification for trust > lists (with no opinion on how they're created or used) so that software > systems like this Vouch project and others can coordinate with each > other. I'm not ready to publish a specification until vouch itself > stabilizes usage more. --- ## File: action/check-issue/README.md # Check Issue Check if an issue author is a vouched contributor. Bots and collaborators with write access are automatically allowed. Denounced users are always blocked. When `require-vouch` is true (default), unvouched users are also blocked. Use `auto-close` to close issues from blocked users. When `auto-lock` is true, closed issues are also locked to prevent further comments. ## Usage ```yaml on: issues: types: [opened, reopened] permissions: contents: read issues: write jobs: check: runs-on: ubuntu-latest steps: - uses: mitchellh/vouch/action/check-issue@v1 with: issue-number: ${{ github.event.issue.number }} auto-close: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` ## Inputs | Name | Required | Default | Description | | --------------- | -------- | ---------------------- | ------------------------------------------------------------ | | `issue-number` | Yes | | GitHub issue number | | `auto-close` | No | `"false"` | Automatically close issues from unvouched or denounced users | | `auto-lock` | No | `"false"` | Automatically lock issues after closing | | `dry-run` | No | `"false"` | Print what would happen without making changes | | `repo` | No | Current repository | Repository in `owner/repo` format | | `require-vouch` | No | `"true"` | Require users to be vouched (false = only block denounced) | | `vouched-file` | No | `".github/VOUCHED.td"` | Path to the vouched contributors file in the repo | ## Outputs | Name | Description | | -------- | ---------------------------------------------------------- | | `status` | Result: `skipped` (bot), `vouched`, `allowed`, or `closed` | --- ## File: action/check-pr/README.md # Check PR Check if a PR author is a vouched contributor. Bots and collaborators with write access are automatically allowed. Denounced users are always blocked. When `require-vouch` is true (default), unvouched users are also blocked. Use `auto-close` to close PRs from blocked users. ## Usage ```yaml on: pull_request_target: types: [opened, reopened] permissions: contents: read pull-requests: write jobs: check: runs-on: ubuntu-latest steps: - uses: mitchellh/vouch/action/check-pr@v1 with: pr-number: ${{ github.event.pull_request.number }} auto-close: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` ## Inputs | Name | Required | Default | Description | | --------------- | -------- | ---------------------- | ---------------------------------------------------------- | | `pr-number` | Yes | | GitHub PR number | | `auto-close` | No | `"false"` | Automatically close PRs from unvouched or denounced users | | `dry-run` | No | `"false"` | Print what would happen without making changes | | `repo` | No | Current repository | Repository in `owner/repo` format | | `require-vouch` | No | `"true"` | Require users to be vouched (false = only block denounced) | | `vouched-file` | No | `".github/VOUCHED.td"` | Path to the vouched contributors file in the repo | ## Outputs | Name | Description | | -------- | ---------------------------------------------------------- | | `status` | Result: `skipped` (bot), `vouched`, `allowed`, or `closed` | --- ## File: action/check-user/README.md # Check User Check if a GitHub user is a vouched contributor. Bots and collaborators with write access are automatically allowed. By default the step fails if the user is denounced or unknown; set `allow-fail` to `true` to always pass and rely on the `status` output instead. ## Usage ```yaml on: workflow_dispatch: inputs: user: required: true permissions: contents: read jobs: check: runs-on: ubuntu-latest steps: - id: vouch uses: mitchellh/vouch/action/check-user@v1 with: user: ${{ github.event.inputs.user }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: echo "Status is ${{ steps.vouch.outputs.status }}" ``` ## Inputs | Name | Required | Default | Description | | -------------- | -------- | ---------------------- | ------------------------------------------------------ | | `user` | Yes | | GitHub username to check | | `allow-fail` | No | `"false"` | Allow the step to pass even if the user is not vouched | | `repo` | No | Current repository | Repository in `owner/repo` format | | `vouched-file` | No | `".github/VOUCHED.td"` | Path to the vouched contributors file in the repo | | `vouched-repo` | No | Same as `repo` | Repository for the vouched file in `owner/repo` format | ## Outputs | Name | Description | | --------- | ------------------------------------------------------------------- | | `status` | Result: `bot`, `collaborator`, `vouched`, `denounced`, or `unknown` | | `vouched` | `true` if the user is vouched (includes bot and collaborator) | --- ## File: action/manage-by-discussion/README.md # Manage by Discussion Manage contributor vouch status via discussion comments. When a collaborator with sufficient permissions comments `vouch` on a discussion, the discussion author is added to the vouched contributors list. When they comment `denounce`, the user is denounced. When they comment `unvouch`, the user is removed from the list entirely. The trigger keywords and required permission levels are configurable. Discussion data (comment body, commenter, discussion author) is fetched via the GitHub GraphQL API since discussions are not available through the REST API. ## Usage ```yaml on: discussion_comment: types: [created] # Serialize updates to the VOUCHED file. concurrency: group: vouch-manage cancel-in-progress: false permissions: contents: write discussions: write jobs: manage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: mitchellh/vouch/action/manage-by-discussion@v1 with: discussion-number: ${{ github.event.discussion.number }} comment-node-id: ${{ github.event.comment.node_id }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` ## Inputs | Name | Required | Default | Description | | ----------------------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `comment-node-id` | Yes | | GraphQL node ID of the discussion comment | | `discussion-number` | Yes | | Discussion number | | `allow-denounce` | No | `"true"` | Enable denounce handling | | `allow-unvouch` | No | `"true"` | Enable unvouch handling | | `allow-vouch` | No | `"true"` | Enable vouch handling | | `denounce-keyword` | No | `""` | Comma-separated keywords that trigger denouncing (default: `denounce`) | | `dry-run` | No | `"false"` | Print what would happen without making changes | | `merge-immediately` | No | `"false"` | Merge the pull request immediately after creation (only applies when `pull-request` is `"true"`) | | `pull-request` | No | `"false"` | Create a pull request instead of pushing directly | | `roles` | No | `""` | Comma-separated role names allowed to manage (default: `admin,maintain,write,triage`). When empty, also accepts the legacy `permission` values `admin` or `write`. | | `repo` | No | `""` | Repository in `owner/repo` format (default: current repository) | | `unvouch-keyword` | No | `""` | Comma-separated keywords that trigger unvouching (default: `unvouch`) | | `vouch-keyword` | No | `""` | Comma-separated keywords that trigger vouching (default: `vouch`) | | `vouched-file` | No | `""` | Path to vouched contributors file (empty = auto-detect) | | `vouched-managers-file` | No | `""` | Path to managers VOUCHED file (empty = disable managers check) | | `vouched-managers-ref` | No | `""` | Git ref for the managers file (empty = default branch) | | `vouched-managers-repo` | No | `""` | Repository in `owner/repo` format for managers file (empty = target repo) | ## Outputs | Name | Description | | -------- | ----------------------------------------------------------- | | `status` | Result: `vouched`, `denounced`, `unvouched`, or `unchanged` | ## Comment Syntax Comments from collaborators with sufficient permissions are matched: - **`vouch`** — vouches for the discussion author (customizable via `vouch-keyword`) - **`vouch @user`** — vouches for a specific user - **`vouch `** — vouches for the discussion author with a reason - **`vouch @user `** — vouches for a specific user with a reason - **`denounce`** — denounces the discussion author (customizable via `denounce-keyword`) - **`denounce @user`** — denounces a specific user - **`denounce `** — denounces the discussion author with a reason - **`denounce @user `** — denounces a specific user with a reason - **`unvouch`** — removes the discussion author (customizable via `unvouch-keyword`) - **`unvouch @user`** — removes a specific user ## Commit Behavior When `dry-run` is `"false"`, the action commits and pushes any changes to the VOUCHED file automatically. The caller must check out the repository before using this action. When `pull-request` is `"true"`, the action creates a new branch and opens a pull request instead of pushing directly to the default branch. This requires `pull-requests: write` permission and a GitHub token that can create pull requests. The default `GITHUB_TOKEN` **cannot** create pull requests unless you enable it under **Settings → Actions → General → Workflow permissions** ("Allow GitHub Actions to create and approve pull requests"). --- ## File: action/manage-by-issue/README.md # Manage by Issue Manage contributor vouch status via issue comments. When a collaborator with sufficient permissions comments `vouch` on an issue, the issue author is added to the vouched contributors list. When they comment `denounce`, the user is denounced. When they comment `unvouch`, the user is removed from the list entirely. The trigger keywords and required permission levels are configurable. ## Usage ```yaml on: issue_comment: types: [created] # Serialize updates to the VOUCHED file. concurrency: group: vouch-manage cancel-in-progress: false permissions: contents: write issues: write pull-requests: read jobs: manage: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: mitchellh/vouch/action/manage-by-issue@v1 with: repo: ${{ github.repository }} issue-id: ${{ github.event.issue.number }} comment-id: ${{ github.event.comment.id }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` ## Inputs | Name | Required | Default | Description | | ----------------------- | -------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `comment-id` | Yes | | GitHub comment ID | | `issue-id` | Yes | | GitHub issue number | | `repo` | Yes | | Repository in `owner/repo` format | | `allow-denounce` | No | `"true"` | Enable denounce handling | | `allow-unvouch` | No | `"true"` | Enable unvouch handling | | `allow-vouch` | No | `"true"` | Enable vouch handling | | `denounce-keyword` | No | `""` | Comma-separated keywords that trigger denouncing (default: `denounce`) | | `dry-run` | No | `"false"` | Print what would happen without making changes | | `merge-immediately` | No | `"false"` | Merge the pull request immediately after creation (only applies when `pull-request` is `"true"`) | | `pull-request` | No | `"false"` | Create a pull request instead of pushing directly | | `roles` | No | `""` | Comma-separated role names allowed to manage (default: `admin,maintain,write,triage`). When empty, also accepts the legacy `permission` values `admin` or `write`. | | `unvouch-keyword` | No | `""` | Comma-separated keywords that trigger unvouching (default: `unvouch`) | | `vouch-keyword` | No | `""` | Comma-separated keywords that trigger vouching (default: `vouch`) | | `vouched-file` | No | `""` | Path to vouched contributors file (empty = auto-detect) | | `vouched-managers-file` | No | `""` | Path to managers VOUCHED file (empty = disable managers check) | | `vouched-managers-ref` | No | `""` | Git ref for the managers file (empty = default branch) | | `vouched-managers-repo` | No | `""` | Repository in `owner/repo` format for managers file (empty = target repo) | ## Outputs | Name | Description | | -------- | ----------------------------------------------------------- | | `status` | Result: `vouched`, `denounced`, `unvouched`, or `unchanged` | ## Comment Syntax Comments from collaborators with sufficient permissions are matched: - **`vouch`** — vouches for the issue author (customizable via `vouch-keyword`) - **`vouch @user`** — vouches for a specific user - **`vouch `** — vouches for the issue author with a reason - **`vouch @user `** — vouches for a specific user with a reason - **`denounce`** — denounces the issue author (customizable via `denounce-keyword`) - **`denounce @user`** — denounces a specific user - **`denounce `** — denounces the issue author with a reason - **`denounce @user `** — denounces a specific user with a reason - **`unvouch`** — removes the issue author (customizable via `unvouch-keyword`) - **`unvouch @user`** — removes a specific user ## Commit Behavior When `dry-run` is `"false"`, the action commits and pushes any changes to the VOUCHED file automatically. The caller must check out the repository before using this action. When `pull-request` is `"true"`, the action creates a new branch and opens a pull request instead of pushing directly to the default branch. This requires `pull-requests: write` permission and a GitHub token that can create pull requests. The default `GITHUB_TOKEN` **cannot** create pull requests unless you enable it under **Settings → Actions → General → Workflow permissions** ("Allow GitHub Actions to create and approve pull requests"). --- ## File: action/setup-vouch/README.md # Setup Vouch Make the `vouch` CLI available on `PATH` for subsequent workflow steps. Nushell is installed automatically if `nu` is not already available. ## Usage ```yaml jobs: example: runs-on: ubuntu-latest steps: - uses: mitchellh/vouch/action/setup-vouch@v1 - run: vouch check someuser ``` ### Capturing output Use `$GITHUB_OUTPUT` to expose the result to later steps: ```yaml - id: check run: echo "status=$(vouch check someuser)" >> "$GITHUB_OUTPUT" - run: echo "${{ steps.check.outputs.status }}" ``` ### Typed arguments Some commands accept typed flags (e.g. `--vouch-keyword`). Pass them using Nushell syntax: ```yaml - run: | vouch gh-manage-by-issue 123 456789 \ --repo owner/repo \ --vouch-keyword [lgtm approve] \ --dry-run=false ``` --- ## File: action/sync-codeowners/README.md # Sync Codeowners Sync CODEOWNERS entries into the VOUCHED list. The action expands any team owners to their members and adds missing users to the vouch file. ## Usage ```yaml on: schedule: - cron: "0 0 * * 1" workflow_dispatch: permissions: contents: write pull-requests: write jobs: sync: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: mitchellh/vouch/action/sync-codeowners@v1 with: repo: ${{ github.repository }} ``` ## Inputs | Name | Required | Default | Description | | ------------------- | -------- | --------- | -------------------------------------------------------------- | | `repo` | No | `""` | Repository in `owner/repo` format (empty = current repository) | | `codeowners-file` | No | `""` | Path to CODEOWNERS file (empty = auto-detect) | | `commit-message` | No | `""` | Commit message override | | `dry-run` | No | `"false"` | Print what would happen without making changes | | `merge-immediately` | No | `"false"` | Merge the pull request immediately after creation | | `pull-request` | No | `"false"` | Create a pull request instead of pushing directly | | `vouched-file` | No | `""` | Path to VOUCHED file (empty = auto-detect) | ## Outputs | Name | Description | | -------- | -------------------------------- | | `status` | Result: `updated` or `unchanged` | ## Notes When `pull-request` is `"true"`, the action creates a branch and opens a pull request instead of pushing directly. This requires `pull-requests: write` permission and a token that can create pull requests. The default `GITHUB_TOKEN` **cannot** create pull requests unless you enable it in the repository settings.