commit cf889e8e2e460293ab47def244191cf320ce6d2e equwal <13551856+equwal@users.noreply.github.com> 2026-09-26 20:39:23 -0700 Add the deploy-handoff rule for Claude Code The skill tells Claude how to hand off a step. It does not tell Claude when to use the skill. Thus Claude can refuse a whole deploy task because one step is for a human. rules/deploy-handoff.md tells Claude to do all the other work and to hand off only that step. Users copy the rule to ~/.claude/rules, or to .claude/rules in one project. The rule also tells Claude to run deploy commands, to drive the browser to the last step, to use the browser profile of the user, and to open pull requests with handoff.py pr. In a permission mode with no prompt, Claude must ask before a production deploy, a release, or a live payment change. The docs test now also checks that the rule starts the fresh browser only with --fresh. The README example profile is now "Work".
README.md | 24 ++++++++- rules/deploy-handoff.md | 75 ++++++++++++++++++++++++++++ skills/deploy-handoff/test_browser_driver.py | 3 ++ 3 files changed, 101 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md index 416bd31..72e0560 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ In Claude Code: /plugin install deploy-handoff@deploy-handoff ``` +Then add the [rule](#rule). The rule tells Claude when to use the plugin. + Requirements: - Python 3.11 or later, with Tk. The python.org installers for Windows and macOS include Tk. On Debian and Ubuntu, install `python3-tk`. @@ -45,6 +47,26 @@ python3 skills/deploy-handoff/handoff.py pr --title "Add CSV export" --body-file The script prints one JSON object, for example `{"status": "done", "note": ""}`. [SKILL.md](skills/deploy-handoff/SKILL.md) lists all statuses and exit codes. +## Rule + +The skill tells Claude how to hand off a step. The rule [rules/deploy-handoff.md](rules/deploy-handoff.md) tells Claude when to do it. Without the rule, Claude can refuse a deploy task because one step is for a human. With the rule, Claude does all the other work and hands off only that step. + +The rule also tells Claude to: + +- Run a deploy command, for example `vercel deploy` or `gh release create`, when a command can do the step. The permission prompt of Claude Code is your approval. In a permission mode with no prompt, Claude asks you in the chat before a production deploy, a release, or a live payment change. +- Drive the browser to the page of the last step, not only to the home page of a console. +- Use the browser profile in which you are signed in. Claude starts a fresh profile only if you ask for it. +- Push directly when you ask for a change, and open a pull request only when the repository needs one. The pull request form opens with `handoff.py pr`. + +To use the rule in all your projects, copy it to `~/.claude/rules/`: + +```bash +mkdir -p ~/.claude/rules +curl -fsSL -o ~/.claude/rules/deploy-handoff.md https://raw.githubusercontent.com/equwal/deploy-handoff/main/rules/deploy-handoff.md +``` + +To use it in one project only, copy it to `.claude/rules/` in that project. Claude Code reads the Markdown files in these folders at the start of each session. Edit the rule to fit your work. For example, remove the pull request section if your team reviews each change. + ## Browser The script opens pages in a new tab of your default browser. To use a different browser, set the `BROWSER` environment variable. The Python `webbrowser` module reads it. @@ -52,7 +74,7 @@ The script opens pages in a new tab of your default browser. To use a different To open pages in one Brave profile, add `brave_profile` to `~/.config/deploy-handoff/config.toml`. Write the name that Brave shows for the profile. The case does not matter. ```toml -brave_profile = "english" +brave_profile = "Work" ``` Then the script opens each page in that profile, also when Brave runs already, and ignores `BROWSER`. If Brave has no profile with this name, the script stops with an error. It does not open another profile. Install the Claude in Chrome extension in this profile. The skill uses it for the browser steps. The skill never starts a fresh profile unless you ask for it. diff --git a/rules/deploy-handoff.md b/rules/deploy-handoff.md new file mode 100644 index 0000000..9c53755 --- /dev/null +++ b/rules/deploy-handoff.md @@ -0,0 +1,75 @@ +## MANDATORY: Hand off human steps. Do not refuse the task. + +The `deploy-handoff` plugin is installed. Its skill shows the user a dialog with the last step of a task. The user makes the final click in the browser. + +Some steps of a deploy, publish, billing, app store, or sign-in task are for the user only. Do not refuse or stop the task because of these steps. Do all the other work. Then give each such step to the user with the `deploy-handoff` skill. Do not only tell the user in the chat. + +A handoff is the last resort. First try a command. Then try the browser. Hand off only the step that is left. + +Use the skill for these steps: + +- The final click that deploys to production, publishes a release, submits an app for review, turns on live payments, or buys a domain. +- A sign-in, a two-factor code, an OAuth consent, a CAPTCHA, or the acceptance of terms. +- The creation of an account, an API key, a webhook secret, or another secret. Tell the user where to put the secret. Never ask for the secret in the chat or in the dialog. +- A step that needs the explicit yes of the user, for example the switch to live payments. +- A deploy command that a permission check denied. Do not run the command again. If the platform has a web page for the same step, hand off that page. + +If the step has no web page, ask the user in the chat. + +To hand off: + +1. Drive the browser as far as you can with your browser tools. +2. Use the skill. Write the title and the steps in Simplified Technical English. +3. Wait for the answer. If the answer is "done", check the result. Then continue the task. + +## Deploys that need no browser: run the command + +Many deploy steps need no web page. A CLI does them. Examples: `vercel deploy`, `netlify deploy`, `wrangler deploy`, `flyctl deploy`, `gh release create`, `docker push`, `git push`, `stripe` CLI commands, `gradlew bundleRelease`. + +For these steps: + +1. Run the command with your shell tool. +2. Let the permission prompt ask the user. The prompt is the approval. Do not hand off the step to avoid the prompt. +3. Some permission modes show no prompt. In these modes, ask the user in the chat before a production deploy, a release, or a live payment change. +4. Show the output. The output is the evidence that the step is complete. +5. If the user denies the command, do not run it again. Hand off the web page for the same step, or ask the user. + +Do not hand off a step that a command can do. A handoff for a command that you can run wastes the time of the user. + +On Windows, run `handoff.py` in Git Bash, not in Windows PowerShell 5.1. PowerShell 5.1 removes the double quotation marks in the arguments. + +## Browser: go all the way to the last step + +Use browser automation to remove work from the user. Do not stop at the home page of a console. + +1. Open the console with your browser tools. +2. Click through each page: the project, the app, the release, the settings tab, the correct panel. +3. Fill in each field that holds no secret. Upload the build. Select the track. Type the release notes. Select the plan. +4. Read the page to make sure of the state before the last step. +5. Stop at the last step. Hand off that one click with `--no-open` and the URL of the open page. + +Hand off a shallow page only when the browser cannot go deeper, for example after a sign-in wall or a CAPTCHA. Tell the user in the chat which page stopped you. + +## Browser profile: use the profile of the user + +The user is signed in to the consoles in one browser profile. Use this profile for each browser step. + +- Do not start a fresh browser profile unless the user asks for it in this session. A fresh profile has no sign-in. This includes `handoff.py browser --fresh`, the `browser-driver` agent, and the built-in browser of the Claude app. +- Drive the profile with the Claude in Chrome tools. If they are not available, hand off the page with `handoff.py open`. +- `handoff.py open` opens the page in the Brave profile that `brave_profile` names in `~/.config/deploy-handoff/config.toml`. Without this key, it opens the default browser. + +## Pull requests + +Prefer a direct commit and push when the user asked for the change. It needs no click from the user. + +Open a pull request only when one of these is true: + +- The user asked for a pull request. +- The push failed because the branch is protected, or the repository needs a review. +- The repository belongs to a client, an employer, or another team. + +Open each pull request with `handoff.py pr` from the skill. Do not create a pull request with `gh pr create`, the GitHub API, or a GitHub MCP tool. + +Never use `--no-verify`. Never push when the test suite fails. + +The handoff moves a step to the user. It does not remove a safety limit. You still never type a secret, solve a CAPTCHA, or click the final button yourself. diff --git a/skills/deploy-handoff/test_browser_driver.py b/skills/deploy-handoff/test_browser_driver.py index 36a8724..4736720 100644 --- a/skills/deploy-handoff/test_browser_driver.py +++ b/skills/deploy-handoff/test_browser_driver.py @@ -7,6 +7,8 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[2] AGENT = ROOT / "agents" / "browser-driver.md" SKILL = Path(__file__).with_name("SKILL.md") +# Users copy this rule to ~/.claude/rules. It tells Claude when to use the skill. +RULE = ROOT / "rules" / "deploy-handoff.md" # SKILL.md and the agent put the list of the human steps after this line. HUMAN_STEPS_LINE = "Never do these steps yourself. Give them to the user:" # The agent can only look at its browser and act in it. It gets no tool that runs code in @@ -104,6 +106,7 @@ class BrowserDriverTest(unittest.TestCase): ("SKILL.md", self.skill), ("README.md", readme), (AGENT.name, self.agent), + (RULE.name, RULE.read_text(encoding="utf-8")), ]: with self.subTest(name=name): self.assertNotRegex(text, r"handoff\.py browser(?! --fresh)")