Automation webhook returns HTTP 400 while scheduled trigger works

Where does the bug appear (feature/product)?

Cloud Agent (GitHub, Slack, Web, Linear)

Describe the Bug

I have a Cursor Automation named “MASTER Autopilot — safe self-pickup” using a multi-repository environment.

The scheduled trigger works correctly and creates successful Cloud Agent runs.

The webhook trigger does not create any run. The authenticated POST reaches Cursor but consistently returns HTTP 400 with:

Unexpected token ‘C’, “Content-Le”… is not valid JSON

The POST body is valid JSON and is sent with:
Content-Type: application/json

I isolated the Automation configuration and reproduced the same error:

  • with the custom GitHub MCP disabled;
  • with the official GitHub Cloud plugin disabled;
  • after removing the custom MCP from the Automation;
  • after removing all Automation tools.

In every case:

  • the scheduled trigger still works;
  • the webhook path fails before a Cursor run is created.

Expected behavior:
An authenticated webhook POST should create an Automation run.

Actual behavior:
HTTP 400 before run creation.

There is no Cursor Request ID because no Cursor run is created by the failing webhook.

GitHub-side dispatcher logs confirm:

  • one POST per test;
  • no retries;
  • the same sanitized error every time.

Platform:
macOS
Cursor Pro

Please investigate the webhook launch/runtime path for this Automation.

Steps to Reproduce

  1. Create or use a Cursor Automation with both a Scheduled trigger and a Webhook trigger.
  2. Configure the Automation with a valid Cloud Agent environment.
  3. Verify that the Scheduled trigger successfully creates and completes Agent runs.
  4. Send an authenticated HTTP POST to the Automation webhook URL with:
    Content-Type: application/json
    and a valid JSON body.
  5. The webhook request returns HTTP 400 before any Cursor run is created.

The error returned is:

Unexpected token ‘C’, “Content-Le”… is not valid JSON

I also reproduced the same webhook error after:

  • disabling the custom GitHub MCP;
  • disabling the official GitHub Cloud plugin;
  • removing the custom MCP from the Automation;
  • removing all Automation tools.

The Scheduled trigger continues to work correctly.

Expected Behavior

The authenticated webhook POST should be accepted and create a new Automation / Cloud Agent run, in the same way that the Scheduled trigger successfully starts the Automation.

Operating System

MacOS

Version Information

Version: 3.20.21
VS Code Extension API: 1.128.0
Commit: f09fca384ceca23f7bf21f9c23655b162641d740
Date: 2026-09-13T19:02:51.781Z
Layout: Agent Window
Build Type: Stable
Release Track: Default
Electron: 42.10.0
Chromium: 148.0.7778.280
Node.js: 24.18.1
V8: 14.8.178.38-electron.0
xterm.js: 6.1.0-beta.291
OS: Darwin arm64 27.0.0

For AI issues: which model did you use?

Cursor Grok 4.6 High Fast

This issue appears to be infrastructure/webhook-related rather than model-specific.

For AI issues: add Request ID with privacy disabled

No Request ID is available because the failing webhook returns HTTP 400 before any Cursor Agent run is created.

Additional Information

The same Automation works correctly when triggered by its scheduled trigger.

A successful scheduled run was observed at 22:14 on Sep 15 and completed successfully.

The webhook path was tested repeatedly from GitHub Actions. Authentication is accepted, but Cursor returns HTTP 400 before creating a run:

Unexpected token ‘C’, “Content-Le”… is not valid JSON

To isolate the issue, I reproduced it with:

  • the custom GitHub MCP disabled;
  • the official GitHub Cloud plugin disabled;
  • the custom MCP removed from the Automation;
  • all Automation tools removed.

The HTTP 400 remained identical in every configuration.

After testing, the Automation configuration was restored.

No webhook URL, authorization token, or other secret is included in this report.

Does this stop you from using Cursor

No - Cursor works, but with this issue

Hey @masterfoundry, thanks for the detailed report.

That 400 is raised while Cursor is parsing the request body, before the request reaches your automation (and before the API key is checked, so a 400 here does not tell us the key was accepted). The error message quotes the first characters of the body Cursor received, and they are the text Content-Le.... In other words, a Content-Length: ... header line is ending up inside the POST body instead of in the HTTP headers. The scheduled trigger works because it never goes through this HTTP request.

This usually comes from how the request is assembled in the workflow step: a heredoc or variable that concatenates header lines into the -d / --data value, headers passed with the wrong flag, or a hand-built HTTP request where the header block and the body are separated incorrectly.

Here is how to pin it down:

  1. From a terminal on your Mac, run this (fill in your automation’s webhook URL and key from the automation’s Webhook trigger settings):
   curl -sS -X POST "https://api2.cursor.sh/automations/webhook/<automation-id>" \
     -H "Authorization: Bearer <your-key>" \
     -H "Content-Type: application/json" \
     -d '{"context":"manual test from the terminal"}'
  1. A working call returns HTTP 200 with a body like {"success":true,"backgroundComposerId":"...","runUuid":"..."}, and a new run appears in the automation’s run history on cursor.com.

  2. To see what your GitHub Actions step is really sending, temporarily point the same step at a request inspector such as https://webhook.site (swap only the URL, keep the headers and body exactly as they are), run the workflow once, and look at the captured request. The body should start with {. If it starts with Content-Length, the fix is in the step that builds the request.

If the plain curl in step 1 also returns this 400 for you, reply with the exact command (key removed) and the full response, and we will take another look!

Hi Colin,

Thank you for your help. I’m replacing my previous message with this consolidated update, as we’ve made significant progress and would like to provide everything needed for a complete diagnosis.

1. Exact failure

On September 16, 2026, at approximately 10:22:57 UTC, our GitHub Actions workflow sent a single POST request to the Cursor automation webhook.

The sanitized response was:

HTTP 400
code=error
message=Unexpected token 'C', "Content-Le"... is not valid JSON.

No retry was attempted, and no Cursor run was confirmed.

As you explained, this error occurs during request-body parsing, before authentication. Our earlier suggestion that authentication had succeeded was incorrect.

2. Request construction

Our workflow uses curl with:

  • POST

  • Content-Type: application/json

  • An Authorization header populated from a GitHub Actions secret

  • --data-binary "$payload"

  • No retries

The payload is generated as compact JSON. The existing configuration check verifies only that the webhook URL and Authorization secret are non-empty; it does not validate their formatting.

3. Tests with synthetic credentials

We extracted the actual curl invocation from our workflow and tested it against a local HTTP receiver using synthetic credentials and a synthetic JSON payload.

With a clean Authorization value:

  • Exactly one POST is sent.

  • The body matches the expected JSON byte for byte.

  • The body begins with {.

  • Content-Length is consistent.

New finding — Authorization ending with CRLF (\r\n):

A raw TCP capture shows that a trailing CRLF in the synthetic Authorization value introduces a premature HTTP header/body separator.

When the request is split at the first \r\n\r\n, the body begins with a line such as:

Content-Length: 108

The first body byte is therefore C, rather than {.

This closely matches the error reported by Cursor:

Unexpected token 'C', "Content-Le"... is not valid JSON

We also confirmed that our current non-empty configuration check would accept an Authorization secret ending in CRLF.

Important limitation: These tests use synthetic credentials. We have not inspected the real secret or the request bytes received by Cursor. The CRLF issue is a reproduced, plausible mechanism — not yet a confirmed root cause.

4. Next steps

We intend to verify or correct the GitHub Actions Authorization secret without exposing its value, then perform one controlled webhook test.

Could your team please:

  1. Check the failed request around 10:22:57 UTC on September 16 and confirm whether its body actually began with Content-Length?

  2. Confirm the exact Authorization header format expected by this webhook.

  3. Let us know whether correcting a trailing CRLF would be sufficient, or whether your server logs indicate another issue.

If the request cannot be identified from the timestamp, please tell us the minimum additional identifier needed and how to provide it securely.

Our repository is private, so we will not publish its URL, webhook endpoint, credentials, original payload or complete workflow logs on this public forum.

Ideally, we’d appreciate a complete diagnosis or a single, clearly defined verification procedure to avoid multiple rounds of troubleshooting.

Thank you again for your assistance.

Hey @masterfoundry, thanks for the detailed follow-up.

We located the request from 10:22:57 UTC and can confirm the body we received began with Content-Length:, exactly as the error says. Nothing on our side moved it there. This is not a proxy issue, and it is not a duplicated or malformed Content-Length / Transfer-Encoding header. We reproduced your exact error from a clean machine, and the cause is a trailing newline in the Authorization header value.

H Your AUTH_VALUE secret almost certainly ends with a line break (common when a secret is created with echo ... | gh secret set, read from a file, or pasted with a trailing Enter). curl on GitHub runners talks to api2.cursor.sh over HTTP/2, and when a header value ends in a newline, curl cuts the header block short at that point. Its own Content-Length: ... line and your JSON are then sent as the request body, so our JSON parser sees Content-Length: 123 first and returns the 400. Authentication never runs, which is why a 400 here does not confirm that the key was accepted.

This also explains why your local tests passed. A plain HTTP receiver speaks HTTP/1.1, and curl only uses HTTP/2 over TLS. In a raw capture the bytes look fine, because the problem is where the header block gets split, not the JSON itself. Over HTTP/1.1 the same stray newline shows up as an empty body instead.

You can confirm the mechanism from a terminal with no credentials (fake automation ID, fake key, no run is created):

curl -sS -X POST "https://api2.cursor.sh/automations/webhook/00000000-0000-0000-0000-000000000000" \
  -H 'Content-Type: application/json' \
  -H $'Authorization: Bearer test\n' \
  --data-binary '{"context":"test"}'

That returns your exact error. Remove the \n and you get 401 Invalid API key, which is what a well-formed request with a wrong key gets.

To check your real secret without printing it, add this to the workflow step before the curl call:

case "$AUTH_VALUE" in
  *$'\n'*|*$'\r'*) echo "AUTH_VALUE contains a line break" ;;
  *) echo "AUTH_VALUE has no line breaks" ;;
esac

The fix is to re-save the secret without the trailing newline (for example printf '%s' 'Bearer <key>' | gh secret set <NAME>), or to strip it in the step:

AUTH_VALUE="$(printf '%s' "$AUTH_VALUE" | tr -d '\r\n')"

Your request structure is otherwise correct: Authorization: Bearer <key>, Content-Type: application/json, the JSON body, posted to the webhook URL from the trigger settings. A newline in the URL secret would fail locally with a curl “URL rejected” error before anything reached us, so that one is fine. A working call returns HTTP 200 with {"success":true,"backgroundComposerId":"...","runUuid":"..."}, and a run appears in the automation’s run history.

If the check above comes back clean and it still fails, reply with the workflow’s curl output (key removed) and we will take another look.

Hi Colin,

Thank you very much for your thorough investigation and clear technical explanation.

We re-saved the GitHub Actions Authorization secret without the trailing newline, leaving the workflow unchanged.

We then ran a controlled test: the webhook returned HTTP 200, GitHub Actions completed successfully, and Cursor’s Automation Run History confirmed a matching webhook-triggered execution with status Succeeded.

Your diagnosis was spot on, and the issue is now resolved on our side.

We really appreciate the time you took to locate our request, reproduce the error, and explain its root cause.

Thanks again for your excellent support!

Best regards,
Oliver