You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A private upload service for agents that need to place files in GitHub pull-request Markdown. It runs on Cloudflare Workers, stores bytes in a private R2 bucket, and returns an opaque public URL plus ready-to-paste Markdown.
3
+
Give coding agents a reliable way to add screenshots, logs, PDFs, and other files to GitHub pull requests.
4
4
5
-
The service does not receive GitHub credentials and does not call GitHub. Anyone possessing an attachment URL can retrieve it, so uploads must be nonsensitive.
5
+
When an agent cannot use GitHub's browser-only attachment interface, this self-hosted service fills the gap. The agent uploads a local file, receives ready-to-paste Markdown, and adds it to a pull request through the normal GitHub API or CLI.
6
6
7
-
## Agent usage
7
+
- Works with Codex, Claude Code, shell scripts, and other automated tools.
8
+
- Runs in your own Cloudflare account using a Worker and a private R2 bucket.
9
+
- Requires no GitHub credentials and never calls GitHub itself.
10
+
- Uses free-tier-conscious storage, request, and retention limits by default.
8
11
9
-
Deploy the service once or configure an existing profile, then expose the included CLI:
12
+
Attachment URLs are public to anyone who possesses them. Use the service only for nonsensitive files.
13
+
14
+
## Demo
15
+
16
+
[Demo PR #1](https://github.com/none23/github-attachments/pull/1) shows the complete workflow. The images in its description were created on two different machines and uploaded by an agent through this service—there was no manual GitHub attachment step.
17
+
18
+
## How it works
19
+
20
+
1. An agent runs the included upload skill or CLI with a local file.
21
+
2. Your Cloudflare Worker authenticates the upload and stores the file in private R2.
22
+
3. The service returns an opaque public URL and ready-to-paste Markdown.
23
+
4. The agent places that Markdown in the pull request body or a comment.
24
+
25
+
```text
26
+
Agent -> your Worker -> private R2
27
+
|
28
+
GitHub PR <--- Markdown URL
29
+
```
30
+
31
+
## Quick start
32
+
33
+
You need:
34
+
35
+
- A Cloudflare account with Workers and R2 available.
36
+
- Node.js 24 or later.
37
+
- A short-lived Cloudflare API token scoped to your account with:
github-attach screenshot.png --alt "Settings after the change"
56
+
$EDITOR"$config_dir/deploy.env"
57
+
chmod 600 "$config_dir/deploy.env"
23
58
```
24
59
25
-
The `token` file contains only the raw upload token. The `url` file contains the Worker origin, such as `https://github-pr-attachments.example.workers.dev`. Neither file uses variable names or quotes. `npm run setup` creates both files automatically.
To override the user token for one repository, add this to that repository's `.env`:
76
+
Setup creates the bucket, lifecycle rule, Worker secret, and user-level upload profile. Once it succeeds, remove or revoke the Cloudflare deployment token unless this machine also needs to administer the service.
The repository `.env` must remain uncommitted. This repository ignores `.env` and `.env.*` by default; copy the same rules into repositories that do not already ignore them. Use `.env.example`for placeholder documentation only—never put a real token in it.
89
+
The symlinks install the same skill for Codex and Claude Code without copying it.
44
90
45
-
The service URL resolves separately:
91
+
### 3. Attach a file
46
92
47
-
| Priority | Location | Scope |
48
-
| ---: | --- | --- |
49
-
| 1 |`GITHUB_ATTACHMENTS_URL` in the process environment | Current process override |
Ask the agent to attach a file to a pull request, or use the CLI directly:
51
94
52
-
The URL is required; there is no shared-service fallback. Repository `.env` cannot redirect a user-level token to another service. User profile files and repository `.env` are parsed as data, never sourced as shell code.
95
+
```bash
96
+
github-attach screenshot.png --alt "Settings after the change"
97
+
```
53
98
54
-
The CLI prints only Markdown by default:
99
+
The command prints Markdown:
55
100
56
101
```markdown
57
102

58
103
```
59
104
60
105
Pass `--json` to receive the complete upload response. Diagnostics and errors go to standard error.
61
106
62
-
The raw API is equally small:
107
+
## Deployment options
108
+
109
+
Each deployment has one Worker, one upload token, and one statically bound R2 bucket. Upload requests cannot select or override the bucket.
110
+
111
+
To reuse an existing private bucket, omit `--create-bucket` and choose a unique prefix:
See [openapi.yaml](openapi.yaml) for the complete contract.
120
+
The default prefix is `<worker-name>/objects/`; the default retention is 180 days. Use `--prefix` and `--retention-days` to change them. Distinct prefixes allow multiple deployments to share one bucket safely.
74
121
75
-
## Architecture
122
+
Setup generates an ignored `wrangler.user.jsonc` and writes the runtime profile to:
- The Worker validates requests, authenticates mutations, generates Markdown, and streams bodies.
83
-
- R2 remains private; there is no `r2.dev` or public bucket endpoint.
84
-
- One SQLite-backed Durable Object is the coordination atom for this single service quota.
85
-
- Durable Object alarms delete expired objects and abandoned reservations.
86
-
- An R2 lifecycle rule provides expiration defense in depth.
129
+
The upload token can use this service but cannot administer Cloudflare.
87
130
88
-
Only PNG, JPEG, GIF, and WebP responses are rendered inline. Every other media type is sent with an attachment disposition, `nosniff`, and a restrictive content security policy.
131
+
## Configuration reference
89
132
90
-
##Limits
133
+
### Upload token
91
134
92
-
| Limit | Default |
93
-
| --- | ---: |
94
-
| Stored data | 8 GB |
95
-
| Objects | 50,000 |
96
-
| Raster image size | 10 MB |
97
-
| Other file size | 25 MB |
98
-
| Upload attempts per day | 1,000 |
99
-
| Upload attempts per month | 100,000 |
100
-
| Retention | 180 days |
135
+
The CLI and agent skill use the first nonempty token in this order:
101
136
102
-
Reservations are atomic, including under concurrent uploads. Failed attempts still count toward daily and monthly operation limits, which keeps abuse accounting conservative.
137
+
| Priority | Location | Scope |
138
+
| ---: | --- | --- |
139
+
| 1 |`GITHUB_ATTACHMENTS_TOKEN` in the repository-root `.env`| Repository override |
140
+
| 2 |`GITHUB_ATTACHMENTS_TOKEN` in the process environment | Process override |
141
+
| 3 |`${XDG_CONFIG_HOME:-~/.config}/github-pr-attachments/token`| User default |
To reuse an existing private bucket, omit `--create-bucket`:
154
-
155
-
```bash
156
-
npm run setup -- \
157
-
--worker github-pr-attachments-yourname \
158
-
--bucket your-existing-private-bucket \
159
-
--prefix github-pr-attachments-yourname/objects/
160
-
```
184
+
- The Worker authenticates mutations, validates requests, streams file bodies, and generates Markdown.
185
+
- R2 remains private; downloads pass through the Worker.
186
+
- A SQLite-backed Durable Object coordinates the service-wide quota.
187
+
- Durable Object alarms clean up expired objects and abandoned reservations.
188
+
- A prefix-scoped R2 lifecycle rule provides expiration defense in depth.
161
189
162
-
The setup command:
190
+
PNG, JPEG, GIF, and WebP responses can render inline. Other media types are served as downloads with `nosniff` and a restrictive content security policy.
163
191
164
-
1. Verifies or creates the named bucket.
165
-
2. Adds a prefix-scoped R2 lifecycle rule.
166
-
3. Generates `wrangler.user.jsonc` with the selected Worker, bucket, prefix, and retention.
167
-
4. Generates a 256-bit upload token and deploys it as a Worker secret.
168
-
5. waits for `/healthz`, then writes the user-level `url` and `token` profile.
192
+
## Default limits
169
193
170
-
The default prefix is `<worker-name>/objects/`; the default retention is 180 days. Use `--prefix` and `--retention-days` to change them. Prefixes let multiple deployments safely share one bucket, provided every deployment uses a distinct prefix. `wrangler.user.jsonc`, `.env*`, `.dev.vars`, and `.github-attachments/` are ignored by Git.
194
+
| Limit | Default |
195
+
| --- | ---: |
196
+
| Stored data | 8 GB |
197
+
| Objects | 50,000 |
198
+
| Raster image size | 10 MB |
199
+
| Other file size | 25 MB |
200
+
| Upload attempts per day | 1,000 |
201
+
| Upload attempts per month | 100,000 |
202
+
| Retention | 180 days |
171
203
172
-
The Cloudflare API token is needed only for provisioning and later redeployment. Remove or revoke it after setup if the machine should retain upload access but not deployment access. The user-level `token` is deliberately separate: it can upload through this service but cannot administer Cloudflare.
204
+
Reservations are atomic under concurrent uploads. Failed attempts still count toward daily and monthly operation limits.
Cloudflare budget alerts should be enabled as defense in depth. They do not stop spending; the application limits, private R2 bucket, and Workers Free request ceiling are the cost boundaries. A dedicated Cloudflare account prevents unrelated usage from consuming the same allowances.
225
+
Cloudflare budget alerts provide additional visibility but do not stop spending. The application limits, private bucket, and platform request limits are the primary cost boundaries.
226
+
227
+
## Local development
228
+
229
+
```bash
230
+
cp .dev.vars.example .dev.vars
231
+
npm install
232
+
npm run types
233
+
npm run check
234
+
npm run dev
235
+
```
236
+
237
+
`npm run check` runs formatting and lint checks, generated binding drift checks, strict TypeScript checks, Workers-runtime integration tests, and CLI/setup tests.
194
238
195
239
## Security model
196
240
197
-
- A 256-bit bearer token protects upload, delete, and quota routes.
241
+
- A random 256-bit bearer token protects upload, delete, and quota routes.
198
242
- Secret comparison uses Cloudflare's constant-time Web Crypto operation.
199
243
- Public IDs are cryptographically random UUIDs and cannot be listed through the service.
200
244
- User filenames never become R2 keys and cannot set response headers.
201
245
- Public URLs are capability URLs, not private-repository authorization.
202
246
- The service does not scan downloads for malware.
203
247
204
-
## Design
248
+
## Design and license
205
249
206
250
See [DESIGN.md](DESIGN.md) for the original proposal and design rationale.
251
+
252
+
This project is available under the [MIT License](LICENSE).
0 commit comments