Send the prompts your developers submit to Cursor's AI to Axia, so sensitive data pasted into an AI coding agent is detected.
A Cursor hook on each developer machine that forwards every submitted prompt to Axia, using the Webhook URL and Webhook Secret you get on the Connect page in Axia.
python3 on the PATH, or Windows with python on the PATH (Python 3, standard library only, nothing to install)Capturing prompts is monitoring of what employees type. Treat it as a policy and consent matter, tell developers before you turn it on, and check it fits how you use Cursor Privacy Mode.
| Value to copy | Looks like |
|---|---|
| Webhook URL | https://<your-axia-host>/api/v1/webhooks/cursor |
| Webhook Secret | A long random string |
Save the Webhook Secret in your password manager now. If you lose it, open the connection on the Connect page and click Regenerate secret, then update every machine: the old secret stops working.
From a developer machine, send a test prompt. Replace the two values with yours:
curl -i -X POST "https://<your-axia-host>/api/v1/webhooks/cursor" \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: <webhook-secret>" \
-d '{"event":"user_prompt_submit","timestamp":"2026-01-01T00:00:00Z","project_dir":"/tmp",
"data":{"user_prompt":"Axia connectivity test","session_id":"setup-test","prompt_id":"1","user":"you@yourcompany.com"}}'
You should get HTTP 202 with "status":"accepted". If not, see Troubleshooting.
The hook reads two environment variables. Cursor must see them when it starts, so set them for the user's login session, not only in a shell profile: Cursor opened from the Dock, Start menu or an app launcher does not read ~/.zshrc, ~/.bashrc or a PowerShell profile.
| Variable | Value |
|---|---|
AXIA_WEBHOOK_URL | The Webhook URL from step 1 |
AXIA_WEBHOOK_SECRET | The Webhook Secret from step 1 |
AXIA_USER_EMAIL (optional) | The developer's work email. See Which user is the prompt attributed to? |
Push a LaunchAgent with your MDM that runs at login, for example /Library/LaunchAgents/com.yourcompany.axia-env.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.yourcompany.axia-env</string>
<key>RunAtLoad</key><true/>
<key>ProgramArguments</key><array>
<string>/bin/sh</string><string>-c</string>
<string>launchctl setenv AXIA_WEBHOOK_URL "https://<your-axia-host>/api/v1/webhooks/cursor"; launchctl setenv AXIA_WEBHOOK_SECRET "<webhook-secret>"</string>
</array>
</dict></plist>
Add the variables to /etc/environment (or a file in /etc/environment.d/), then have the user log out and back in.
Set them as system environment variables, with Intune or another MDM, or from an Administrator command prompt:
setx /M AXIA_WEBHOOK_URL "https://<your-axia-host>/api/v1/webhooks/cursor"
setx /M AXIA_WEBHOOK_SECRET "<webhook-secret>"
Have the user sign out and back in, so Cursor starts with the new variables.
Cursor reads hooks.json from several places. Use the enterprise location for production: developers cannot remove it. Use the per-user location to try it on your own machine first.
| Scope | Path | Use for |
|---|---|---|
| Enterprise (macOS) | /Library/Application Support/Cursor/hooks.json | Fleet rollout by MDM |
| Enterprise (Linux) | /etc/cursor/hooks.json | Fleet rollout by MDM |
| Enterprise (Windows) | C:\ProgramData\Cursor\hooks.json | Fleet rollout by MDM |
| Team | Cursor dashboard (Cursor Enterprise, see below) | Fleet rollout without MDM |
| User | ~/.cursor/hooks.json | Testing on one machine |
The file contents differ by operating system, because Cursor runs hooks with PowerShell on Windows. If a hooks.json already exists there, add the beforeSubmitPrompt entry to it.
{
"version": 1,
"hooks": {
"beforeSubmitPrompt": [
{
"timeout": 10,
"failClosed": false,
"command": "python3 -c \"import os,sys,json,shutil,subprocess,getpass,datetime as dt;s=os.environ.get('AXIA_WEBHOOK_SECRET','');url=os.environ.get('AXIA_WEBHOOK_URL','');(s and url) or sys.exit(0);d=json.load(sys.stdin);e=(subprocess.run(['git','config','user.email'],capture_output=True,text=True).stdout.strip() if shutil.which('git') else '');r=d.get('workspace_roots') or [''];u=os.environ.get('AXIA_USER_EMAIL') or e or d.get('user_email') or getpass.getuser();body=json.dumps({'event':'user_prompt_submit','timestamp':dt.datetime.now(dt.timezone.utc).isoformat(),'project_dir':(r[0] if r else ''),'data':{'user_prompt':d.get('prompt',''),'session_id':d.get('conversation_id',''),'prompt_id':d.get('generation_id',''),'user':u}});p=subprocess.Popen([sys.executable,'-c','import sys,json,urllib.request as r;m=json.loads(sys.stdin.buffer.readline());r.urlopen(r.Request(m[0],data=sys.stdin.buffer.read(),headers=m[1]),timeout=5)'],stdin=subprocess.PIPE,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,**({'creationflags':0x00000208} if os.name=='nt' else {'start_new_session':True}));p.stdin.write(json.dumps([url,{'Content-Type':'application/json','X-Webhook-Secret':s}]).encode()+bytes([10])+body.encode());p.stdin.close()\" 2>/dev/null; echo '{\"continue\": true}'"
}
]
}
}
{
"version": 1,
"hooks": {
"beforeSubmitPrompt": [
{
"timeout": 10,
"failClosed": false,
"command": "python -c 'import os,sys,json,shutil,subprocess,getpass,datetime as dt;s=os.environ.get(''AXIA_WEBHOOK_SECRET'','''');url=os.environ.get(''AXIA_WEBHOOK_URL'','''');(s and url) or sys.exit(0);d=json.load(sys.stdin);e=(subprocess.run([''git'',''config'',''user.email''],capture_output=True,text=True).stdout.strip() if shutil.which(''git'') else '''');r=d.get(''workspace_roots'') or [''''];u=os.environ.get(''AXIA_USER_EMAIL'') or e or d.get(''user_email'') or getpass.getuser();body=json.dumps({''event'':''user_prompt_submit'',''timestamp'':dt.datetime.now(dt.timezone.utc).isoformat(),''project_dir'':(r[0] if r else ''''),''data'':{''user_prompt'':d.get(''prompt'',''''),''session_id'':d.get(''conversation_id'',''''),''prompt_id'':d.get(''generation_id'',''''),''user'':u}});p=subprocess.Popen([sys.executable,''-c'',''import sys,json,urllib.request as r;m=json.loads(sys.stdin.buffer.readline());r.urlopen(r.Request(m[0],data=sys.stdin.buffer.read(),headers=m[1]),timeout=5)''],stdin=subprocess.PIPE,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,**({''creationflags'':0x00000208} if os.name==''nt'' else {''start_new_session'':True}));p.stdin.write(json.dumps([url,{''Content-Type'':''application/json'',''X-Webhook-Secret'':s}]).encode()+bytes([10])+body.encode());p.stdin.close()' 2>$null; Write-Output '{\"continue\": true}'"
}
]
}
}
failClosed offWith "failClosed": false, a network error or a missing Python never stops a developer's prompt. The hook also always prints {"continue": true}, so Axia only observes: it never blocks.
On Cursor Enterprise you can add the hook in the Cursor web dashboard instead of deploying a file. Cursor syncs it to every team member automatically, about every 30 minutes. You still need to set the variables on each machine (step 3).
macOS and Linux:
#!/bin/sh
python3 -c "import os,sys,json,shutil,subprocess,getpass,datetime as dt;s=os.environ.get('AXIA_WEBHOOK_SECRET','');url=os.environ.get('AXIA_WEBHOOK_URL','');(s and url) or sys.exit(0);d=json.load(sys.stdin);e=(subprocess.run(['git','config','user.email'],capture_output=True,text=True).stdout.strip() if shutil.which('git') else '');r=d.get('workspace_roots') or [''];u=os.environ.get('AXIA_USER_EMAIL') or e or d.get('user_email') or getpass.getuser();body=json.dumps({'event':'user_prompt_submit','timestamp':dt.datetime.now(dt.timezone.utc).isoformat(),'project_dir':(r[0] if r else ''),'data':{'user_prompt':d.get('prompt',''),'session_id':d.get('conversation_id',''),'prompt_id':d.get('generation_id',''),'user':u}});p=subprocess.Popen([sys.executable,'-c','import sys,json,urllib.request as r;m=json.loads(sys.stdin.buffer.readline());r.urlopen(r.Request(m[0],data=sys.stdin.buffer.read(),headers=m[1]),timeout=5)'],stdin=subprocess.PIPE,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,**({'creationflags':0x00000208} if os.name=='nt' else {'start_new_session':True}));p.stdin.write(json.dumps([url,{'Content-Type':'application/json','X-Webhook-Secret':s}]).encode()+bytes([10])+body.encode());p.stdin.close()" 2>/dev/null; echo '{"continue": true}'
Windows (PowerShell):
python -c 'import os,sys,json,shutil,subprocess,getpass,datetime as dt;s=os.environ.get(''AXIA_WEBHOOK_SECRET'','''');url=os.environ.get(''AXIA_WEBHOOK_URL'','''');(s and url) or sys.exit(0);d=json.load(sys.stdin);e=(subprocess.run([''git'',''config'',''user.email''],capture_output=True,text=True).stdout.strip() if shutil.which(''git'') else '''');r=d.get(''workspace_roots'') or [''''];u=os.environ.get(''AXIA_USER_EMAIL'') or e or d.get(''user_email'') or getpass.getuser();body=json.dumps({''event'':''user_prompt_submit'',''timestamp'':dt.datetime.now(dt.timezone.utc).isoformat(),''project_dir'':(r[0] if r else ''''),''data'':{''user_prompt'':d.get(''prompt'',''''),''session_id'':d.get(''conversation_id'',''''),''prompt_id'':d.get(''generation_id'',''''),''user'':u}});p=subprocess.Popen([sys.executable,''-c'',''import sys,json,urllib.request as r;m=json.loads(sys.stdin.buffer.readline());r.urlopen(r.Request(m[0],data=sys.stdin.buffer.read(),headers=m[1]),timeout=5)''],stdin=subprocess.PIPE,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,**({''creationflags'':0x00000208} if os.name==''nt'' else {''start_new_session'':True}));p.stdin.write(json.dumps([url,{''Content-Type'':''application/json'',''X-Webhook-Secret'':s}]).encode()+bytes([10])+body.encode());p.stdin.close()' 2>$null; Write-Output '{"continue": true}'
Dashboard hooks don't take a failClosed option. They fail open by default, so the prompt still goes through if the hook fails.
Restart Cursor after deploying. In Cursor, the Hooks tab under Customize lists the hook and shows its output if it fails.
The hook works out the developer's identity in this order:
AXIA_USER_EMAIL environment variable, if setgit config user.emailGit email comes before the Cursor account so that one person's Cursor and Claude Code prompts match up in Axia.
| Symptom | Cause | Fix |
|---|---|---|
Test returns 404 Unknown source | Cursor is not connected in Axia, or the URL is wrong | Connect Cursor (step 1) and copy the Webhook URL exactly |
Test returns 401 | Wrong or missing secret | Paste the secret again, or regenerate it on the Connect page and update every machine |
| Test times out | The machine can't reach Axia | Check VPN, proxy and firewall rules for the Axia host |
| Test works, but no prompts arrive | Cursor started without the variables, or the hook isn't loaded | Check step 3 applied to the login session (on macOS: launchctl getenv AXIA_WEBHOOK_URL; on Windows: echo %AXIA_WEBHOOK_URL% in a new command prompt), quit and reopen Cursor, check Customize > Hooks |
| No prompts arrive from Windows machines | Python is missing, or python opens the Microsoft Store placeholder | Install Python 3 from python.org with Add python.exe to PATH, then check python --version in a new terminal |
| Prompts are attributed to a login name, not an email | No AXIA_USER_EMAIL, git email or Cursor account email | Set AXIA_USER_EMAIL or the developer's git email |
curl from a developer machine returns 202AXIA_WEBHOOK_URL and AXIA_WEBHOOK_SECRET set for the login session on every machinehooks.json deployed with "failClosed": false